Repository: microsoft/electionguard-python Branch: main Commit: bc3fcb7da9d8 Files: 303 Total size: 1.4 MB Directory structure: gitextract_dl9na5eh/ ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yml │ │ ├── config.yml │ │ └── enhancement.yml │ ├── pull_request_template.md │ └── workflows/ │ ├── pull_request.yml │ └── release.yml ├── .gitignore ├── .vscode/ │ └── extensions.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── SECURITY.md ├── data/ │ ├── ballot_in_simple.json │ ├── election_manifest_simple.json │ ├── manifest-full.json │ ├── manifest-hamilton-general.json │ ├── manifest-minimal.json │ ├── manifest-small.json │ ├── plaintext_ballots_simple.json │ ├── plaintext_two_ballots_minimal.json │ └── plaintext_two_ballots_small.json ├── docs/ │ ├── 0_Configure_Election.ipynb │ ├── 1_Key_Ceremony.md │ ├── 2_Encrypt_Ballots.md │ ├── 3_Cast_and_Spoil.md │ ├── 4_Decrypt_Tally.md │ ├── 5_Publish_and_Verify.md │ ├── Build_and_Run.md │ ├── Design_and_Architecture.md │ ├── Election_Manifest.md │ ├── Project_Workflow.md │ ├── Tablet Setup.md │ └── index.md ├── mkdocs.yml ├── pyproject.toml ├── src/ │ ├── electionguard/ │ │ ├── __init__.py │ │ ├── ballot.py │ │ ├── ballot_box.py │ │ ├── ballot_code.py │ │ ├── ballot_compact.py │ │ ├── ballot_validator.py │ │ ├── big_integer.py │ │ ├── byte_padding.py │ │ ├── chaum_pedersen.py │ │ ├── constants.py │ │ ├── data_store.py │ │ ├── decrypt_with_secrets.py │ │ ├── decrypt_with_shares.py │ │ ├── decryption.py │ │ ├── decryption_mediator.py │ │ ├── decryption_share.py │ │ ├── discrete_log.py │ │ ├── election.py │ │ ├── election_object_base.py │ │ ├── election_polynomial.py │ │ ├── elgamal.py │ │ ├── encrypt.py │ │ ├── group.py │ │ ├── guardian.py │ │ ├── hash.py │ │ ├── hmac.py │ │ ├── key_ceremony.py │ │ ├── key_ceremony_mediator.py │ │ ├── logs.py │ │ ├── manifest.py │ │ ├── nonces.py │ │ ├── proof.py │ │ ├── py.typed │ │ ├── scheduler.py │ │ ├── schnorr.py │ │ ├── serialize.py │ │ ├── singleton.py │ │ ├── tally.py │ │ ├── type.py │ │ └── utils.py │ ├── electionguard_cli/ │ │ ├── __init__.py │ │ ├── cli_models/ │ │ │ ├── __init__.py │ │ │ ├── cli_decrypt_results.py │ │ │ ├── cli_election_inputs_base.py │ │ │ ├── e2e_build_election_results.py │ │ │ ├── encrypt_results.py │ │ │ ├── mark_results.py │ │ │ └── submit_results.py │ │ ├── cli_steps/ │ │ │ ├── __init__.py │ │ │ ├── cli_step_base.py │ │ │ ├── decrypt_step.py │ │ │ ├── election_builder_step.py │ │ │ ├── encrypt_votes_step.py │ │ │ ├── input_retrieval_step_base.py │ │ │ ├── key_ceremony_step.py │ │ │ ├── mark_ballots_step.py │ │ │ ├── output_step_base.py │ │ │ ├── print_results_step.py │ │ │ ├── submit_ballots_step.py │ │ │ └── tally_step.py │ │ ├── e2e/ │ │ │ ├── __init__.py │ │ │ ├── e2e_command.py │ │ │ ├── e2e_election_builder_step.py │ │ │ ├── e2e_input_retrieval_step.py │ │ │ ├── e2e_inputs.py │ │ │ ├── e2e_publish_step.py │ │ │ └── submit_votes_step.py │ │ ├── encrypt_ballots/ │ │ │ ├── __init__.py │ │ │ ├── encrypt_ballot_inputs.py │ │ │ ├── encrypt_ballots_election_builder_step.py │ │ │ ├── encrypt_ballots_input_retrieval_step.py │ │ │ ├── encrypt_ballots_publish_step.py │ │ │ └── encrypt_command.py │ │ ├── import_ballots/ │ │ │ ├── __init__.py │ │ │ ├── import_ballot_inputs.py │ │ │ ├── import_ballots_command.py │ │ │ ├── import_ballots_election_builder_step.py │ │ │ ├── import_ballots_input_retrieval_step.py │ │ │ └── import_ballots_publish_step.py │ │ ├── mark_ballots/ │ │ │ ├── __init__.py │ │ │ ├── mark_ballot_inputs.py │ │ │ ├── mark_ballots_election_builder_step.py │ │ │ ├── mark_ballots_input_retrieval_step.py │ │ │ ├── mark_ballots_publish_step.py │ │ │ └── mark_command.py │ │ ├── setup_election/ │ │ │ ├── __init__.py │ │ │ ├── output_setup_files_step.py │ │ │ ├── setup_election_builder_step.py │ │ │ ├── setup_election_command.py │ │ │ ├── setup_input_retrieval_step.py │ │ │ └── setup_inputs.py │ │ ├── start.py │ │ └── submit_ballots/ │ │ ├── __init__.py │ │ ├── submit_ballot_inputs.py │ │ ├── submit_ballots_election_builder_step.py │ │ ├── submit_ballots_input_retrieval_step.py │ │ ├── submit_ballots_publish_step.py │ │ └── submit_command.py │ ├── electionguard_db/ │ │ ├── docker-compose.db.yml │ │ └── mongo-init.js │ ├── electionguard_gui/ │ │ ├── .dockerignore │ │ ├── Dockerfile │ │ ├── __init__.py │ │ ├── components/ │ │ │ ├── __init__.py │ │ │ ├── component_base.py │ │ │ ├── create_decryption_component.py │ │ │ ├── create_election_component.py │ │ │ ├── create_key_ceremony_component.py │ │ │ ├── election_list_component.py │ │ │ ├── export_election_record_component.py │ │ │ ├── export_encryption_package_component.py │ │ │ ├── guardian_home_component.py │ │ │ ├── key_ceremony_details_component.py │ │ │ ├── upload_ballots_component.py │ │ │ ├── view_decryption_component.py │ │ │ ├── view_election_component.py │ │ │ ├── view_spoiled_ballot_component.py │ │ │ └── view_tally_component.py │ │ ├── containers.py │ │ ├── docker-compose.yml │ │ ├── eel_utils.py │ │ ├── main_app.py │ │ ├── models/ │ │ │ ├── __init__.py │ │ │ ├── decryption_dto.py │ │ │ ├── election_dto.py │ │ │ ├── key_ceremony_dto.py │ │ │ └── key_ceremony_states.py │ │ ├── services/ │ │ │ ├── __init__.py │ │ │ ├── authorization_service.py │ │ │ ├── ballot_upload_service.py │ │ │ ├── configuration_service.py │ │ │ ├── db_serialization_service.py │ │ │ ├── db_service.py │ │ │ ├── db_watcher_service.py │ │ │ ├── decryption_service.py │ │ │ ├── decryption_stages/ │ │ │ │ ├── __init__.py │ │ │ │ ├── decryption_s1_join_service.py │ │ │ │ ├── decryption_s2_announce_service.py │ │ │ │ └── decryption_stage_base.py │ │ │ ├── directory_service.py │ │ │ ├── eel_log_service.py │ │ │ ├── election_service.py │ │ │ ├── export_service.py │ │ │ ├── guardian_service.py │ │ │ ├── gui_setup_input_retrieval_step.py │ │ │ ├── key_ceremony_service.py │ │ │ ├── key_ceremony_stages/ │ │ │ │ ├── __init__.py │ │ │ │ ├── key_ceremony_s1_join_service.py │ │ │ │ ├── key_ceremony_s2_announce_service.py │ │ │ │ ├── key_ceremony_s3_make_backup_service.py │ │ │ │ ├── key_ceremony_s4_share_backup_service.py │ │ │ │ ├── key_ceremony_s5_verify_backup_service.py │ │ │ │ ├── key_ceremony_s6_publish_key_service.py │ │ │ │ └── key_ceremony_stage_base.py │ │ │ ├── key_ceremony_state_service.py │ │ │ ├── plaintext_ballot_service.py │ │ │ ├── service_base.py │ │ │ └── version_service.py │ │ ├── start.py │ │ └── web/ │ │ ├── components/ │ │ │ ├── admin/ │ │ │ │ ├── admin-home-component.js │ │ │ │ ├── create-decryption-component.js │ │ │ │ ├── create-election-component.js │ │ │ │ ├── create-key-ceremony-component.js │ │ │ │ ├── export-election-record-component.js │ │ │ │ ├── export-encryption-package-component.js │ │ │ │ ├── upload-ballots-component.js │ │ │ │ ├── upload-ballots-legacy-component.js │ │ │ │ ├── upload-ballots-success-component.js │ │ │ │ ├── upload-ballots-wizard-component.js │ │ │ │ ├── view-decryption-admin-component.js │ │ │ │ ├── view-election-component.js │ │ │ │ ├── view-key-ceremony-component.js │ │ │ │ ├── view-spoiled-ballot-component.js │ │ │ │ └── view-tally-component.js │ │ │ ├── guardian/ │ │ │ │ ├── decryption-list-component.js │ │ │ │ ├── guardian-home-component.js │ │ │ │ ├── view-decryption-guardian-component.js │ │ │ │ └── view-key-ceremony-component.js │ │ │ └── shared/ │ │ │ ├── election-list-component.js │ │ │ ├── footer-component.js │ │ │ ├── home-component.js │ │ │ ├── key-ceremony-details-component.js │ │ │ ├── key-ceremony-list-component.js │ │ │ ├── login-component.js │ │ │ ├── navbar-component.js │ │ │ ├── not-found-component.js │ │ │ ├── spinner-component.js │ │ │ └── view-plaintext-ballot-component.js │ │ ├── css/ │ │ │ ├── bootstrap-icons.css │ │ │ ├── bootstrap-overrides.css │ │ │ ├── eg-styles.css │ │ │ └── spinner.css │ │ ├── index.html │ │ ├── js/ │ │ │ └── vue.esm-browser.prod.js │ │ ├── services/ │ │ │ ├── authorization-service.js │ │ │ └── router-service.js │ │ └── site.webmanifest │ ├── electionguard_tools/ │ │ ├── __init__.py │ │ ├── factories/ │ │ │ ├── __init__.py │ │ │ ├── ballot_factory.py │ │ │ └── election_factory.py │ │ ├── helpers/ │ │ │ ├── __init__.py │ │ │ ├── election_builder.py │ │ │ ├── export.py │ │ │ ├── key_ceremony_orchestrator.py │ │ │ ├── tally_accumulate.py │ │ │ └── tally_ceremony_orchestrator.py │ │ ├── scripts/ │ │ │ ├── __init__.py │ │ │ └── sample_generator.py │ │ └── strategies/ │ │ ├── __init__.py │ │ ├── election.py │ │ ├── elgamal.py │ │ └── group.py │ └── electionguard_verify/ │ ├── __init__.py │ └── verify.py ├── stubs/ │ └── gmpy2.pyi └── tests/ ├── __init__.py ├── base_test_case.py ├── bench/ │ ├── __init__.py │ └── bench_chaum_pedersen.py ├── integration/ │ ├── __init__.py │ ├── test_create_schema.py │ ├── test_end_to_end_election.py │ ├── test_functional_key_ceremony.py │ └── test_hamilton_county_election.py ├── property/ │ ├── __init__.py │ ├── test_ballot.py │ ├── test_chaum_pedersen.py │ ├── test_decrypt_with_secrets.py │ ├── test_decryption_mediator.py │ ├── test_discrete_log.py │ ├── test_elgamal.py │ ├── test_encrypt.py │ ├── test_encrypt_hypotheses.py │ ├── test_group.py │ ├── test_hash.py │ ├── test_nonces.py │ ├── test_schnorr.py │ ├── test_tally.py │ └── test_verify.py └── unit/ ├── __init__.py ├── electionguard/ │ ├── __init__.py │ ├── test_ballot.py │ ├── test_ballot_box.py │ ├── test_ballot_code.py │ ├── test_ballot_compact.py │ ├── test_constants.py │ ├── test_decrypt_with_shares.py │ ├── test_decryption.py │ ├── test_election_polynomial.py │ ├── test_elgamal.py │ ├── test_encrypt.py │ ├── test_guardian.py │ ├── test_hmac.py │ ├── test_key_ceremony.py │ ├── test_key_ceremony_mediator.py │ ├── test_logs.py │ ├── test_manifest.py │ ├── test_scheduler.py │ ├── test_singleton.py │ └── test_utils.py └── electionguard_gui/ ├── __init__.py ├── test_decryption_dto.py ├── test_eel_utils.py ├── test_election_dto.py └── test_plaintext_ballot_service.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yml ================================================ name: 🐞 Bug description: Submit a bug if something isn't working as expected. title: "🐞 " labels: [bug, triage] body: - type: checkboxes attributes: label: Is there an existing issue for this? description: Please search to see if an issue already exists for the bug you encountered. options: - label: I have searched the existing issues required: true - type: textarea attributes: label: Current Behavior description: A concise description of what you're experiencing. validations: required: true - type: textarea attributes: label: Expected Behavior description: A concise description of what you expected to happen. validations: required: false - type: textarea attributes: label: Steps To Reproduce description: Steps to reproduce the behavior. placeholder: | 1. In this environment... 2. With this config... 3. Run '...' 4. See error... validations: required: false - type: textarea attributes: label: Environment description: | examples: - **OS**: Ubuntu 20.04 value: | - OS: render: markdown validations: required: false - type: textarea attributes: label: Anything else? description: | Links? References? Screenshots? Possible Solution? Anything that will give us more context about the issue you are encountering! Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. validations: required: false ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: Discussions url: https://github.com/microsoft/electionguard/discussions about: Discuss suggestions and new enhancements here. - name: ElectionGuard Info url: https://https://www.electionguard.vote/ about: Learn more about ElectionGuard or email the team. ================================================ FILE: .github/ISSUE_TEMPLATE/enhancement.yml ================================================ name: ✨ Enhancement description: Suggest an enhancement or an improvement. title: "✨ <title>" labels: [enhancement, triage] body: - type: checkboxes attributes: label: Is there an existing issue for this? description: Please search to see if an issue already exists for the suggestion. options: - label: I have searched the existing issues required: true - type: textarea attributes: label: Suggestion description: Tell us how we could improve ElectionGuard. validations: required: true - type: textarea attributes: label: Possible Implementation description: Not obligatory, but ideas as to the implementation of the suggestion. validations: required: false - type: textarea attributes: label: Anything else? description: | What are you trying to accomplish? Links? References? Anything that will give us more context about the suggestion! Tip: You can attach images by clicking this area to highlight it and then dragging files in. validations: required: false ================================================ FILE: .github/pull_request_template.md ================================================ [//]: # (🚨 Please review the CONTRIBUTING.md in this repository. 💔Thank you!) ### Issue *Link your PR to an issue* Fixes #___ ### Description *Please describe your pull request.* ### Testing *Describe the best way to test or validate your PR.* ================================================ FILE: .github/workflows/pull_request.yml ================================================ name: Validate Pull Request on: push: branches: [main, "integration/**", "releases/**"] pull_request: branches: [main, "integration/**", "releases/**"] repository_dispatch: types: [pull_request] schedule: - cron: "30 23 * * 1" # 2330 UTC Every Monday env: PYTHON_VERSION: 3.9 POETRY_PATH: "$HOME/.poetry/bin" jobs: code_analysis: name: Code Analysis runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: ["python"] steps: - name: Checkout Code uses: actions/checkout@v2 - name: Set up Python ${{ env.PYTHON_VERSION }} uses: actions/setup-python@v2 with: python-version: ${{ env.PYTHON_VERSION }} - name: Change Directory run: cd ${{ github.workspace }} - name: Setup Environment run: make environment - name: Add Poetry Path run: echo ${{ env.POETRY_PATH }} >> $GITHUB_PATH - name: Install Dependencies run: make install - name: Lint run: make lint - name: Initialize CodeQL uses: github/codeql-action/init@v2 with: languages: "${{ matrix.language }}" - name: Autobuild uses: github/codeql-action/autobuild@v2 - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 linux_check: name: Linux Check runs-on: ubuntu-latest steps: - name: Checkout Code uses: actions/checkout@v2 - name: Set up Python ${{ env.PYTHON_VERSION }} uses: actions/setup-python@v2 with: python-version: ${{ env.PYTHON_VERSION }} - name: Change Directory run: cd ${{ github.workspace }} - name: Setup Environment run: make environment - name: Add Poetry Path run: echo ${{ env.POETRY_PATH }} >> $GITHUB_PATH - name: Install Dependencies run: make install - name: Build run: make build validate - name: Full Test Suite & Coverage uses: nick-fields/retry@v2 with: timeout_minutes: 10 max_attempts: 1 retry_on: timeout command: make coverage mac_check: name: MacOS Check runs-on: macos-latest steps: - name: Checkout Code uses: actions/checkout@v2 - name: Set up Python ${{ env.PYTHON_VERSION }} uses: actions/setup-python@v2 with: python-version: ${{ env.PYTHON_VERSION }} - name: Change Directory run: cd ${{ github.workspace }} - name: Setup Environment run: make environment - name: Add Poetry Path run: echo ${{ env.POETRY_PATH }} >> $GITHUB_PATH - name: Install Dependencies run: make install - name: Build run: make build validate - name: Integration Tests uses: nick-fields/retry@v2 with: timeout_minutes: 3 max_attempts: 1 retry_on: timeout command: make test-integration ================================================ FILE: .github/workflows/release.yml ================================================ name: Release Build on: milestone: types: [closed] env: PYTHON_VERSION: 3.9 POETRY_PATH: "$HOME/.poetry/bin" jobs: code_analysis: name: Code Analysis runs-on: ubuntu-latest steps: - name: Checkout Code uses: actions/checkout@v2 - name: Set up Python ${{ env.PYTHON_VERSION }} uses: actions/setup-python@v2 with: python-version: ${{ env.PYTHON_VERSION }} - name: Change Directory run: cd ${{ github.workspace }} - name: Setup Environment run: make environment - name: Add Poetry Path run: echo ${{ env.POETRY_PATH }} >> $GITHUB_PATH - name: Install Dependencies run: make install - name: Lint run: make lint - name: Initialize CodeQL uses: github/codeql-action/init@v2 with: languages: python - name: Autobuild uses: github/codeql-action/autobuild@v2 - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 linux_check: name: Linux Check runs-on: ubuntu-latest steps: - name: Checkout Code uses: actions/checkout@v2 - name: Set up Python ${{ env.PYTHON_VERSION }} uses: actions/setup-python@v2 with: python-version: ${{ env.PYTHON_VERSION }} - name: Change Directory run: cd ${{ github.workspace }} - name: Setup Environment run: make environment - name: Add Poetry Path run: echo ${{ env.POETRY_PATH }} >> $GITHUB_PATH - name: Install Dependencies run: make install - name: Build run: make build validate - name: Full Test Suite & Coverage run: make coverage mac_check: name: MacOS Check runs-on: macos-latest steps: - name: Checkout Code uses: actions/checkout@v2 - name: Set up Python ${{ env.PYTHON_VERSION }} uses: actions/setup-python@v2 with: python-version: ${{ env.PYTHON_VERSION }} - name: Change Directory run: cd ${{ github.workspace }} - name: Setup Environment run: make environment - name: Add Poetry Path run: echo ${{ env.POETRY_PATH }} >> $GITHUB_PATH - name: Install Dependencies run: make install - name: Build run: make build validate - name: Integration Tests run: make test-integration release: name: Release runs-on: ubuntu-latest needs: [code_analysis, mac_check, linux_check] steps: - name: Checkout Code uses: actions/checkout@v2 - name: Set up Python ${{ env.PYTHON_VERSION }} uses: actions/setup-python@v1 with: python-version: ${{ env.PYTHON_VERSION }} - name: Change Directory run: cd ${{ github.workspace }} - name: Setup Environment run: make environment - name: Add Poetry Path run: echo ${{ env.POETRY_PATH }} >> $GITHUB_PATH - name: Install Dependencies run: make install - name: Get Version run: echo "PACKAGE_VERSION=$(echo $VERSION | poetry version --short)" >> $GITHUB_ENV - name: Generate release notes run: make release-notes - name: Create Release id: create_release uses: actions/create-release@v1.0.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag_name: ${{ env.PACKAGE_VERSION }} release_name: Release ${{ env.PACKAGE_VERSION }} draft: false prerelease: false - name: Dependency Graph run: make dependency-graph-ci - name: Upload Dependency Graph as Artifact uses: actions/upload-artifact@v2 with: name: dependency-graph path: dependency-graph.svg - name: Build run: make build - name: Upload Package as Artifact uses: actions/upload-artifact@v2 with: name: package path: dist/ - name: Upload Package to PyPi env: TEST_PYPI_TOKEN: ${{ secrets.TEST_PYPI_TOKEN }} PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} run: make publish-ci - name: Zip Artifacts run: make release-zip-ci - name: Add Artifacts to Release id: upload-release-asset_1 uses: actions/upload-release-asset@v1.0.1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ steps.create_release.outputs.upload_url }} asset_path: ./electionguard.zip asset_name: electionguard.zip asset_content_type: application/zip - name: Deploy Github Pages run: make docs-deploy-ci ================================================ FILE: .gitignore ================================================ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so data/0.95.0/ data/1.0/ # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ coverage/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # PyCharm .idea/ coverage/ cov.xml .DS_Store # File output election_record election_private_data guardian_private_data.json election_record.zip election_private_data.zip schemas # VS Code .vscode/settings.json sample-data.zip* results/ gui_private_keys/ database/ egui_mnt/ ================================================ FILE: .vscode/extensions.json ================================================ { // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp // List of extensions which should be recommended for users of this workspace. "recommendations": [ "ms-python.python", "visualstudioexptteam.vscodeintellicode", "magicstack.magicpython", "hbenl.vscode-test-explorer", "littlefoxteam.vscode-python-test-adapter", "cschleiden.vscode-github-actions", "bungcip.better-toml" ], // List of extensions recommended by VS Code that should not be recommended for users of this workspace. "unwantedRecommendations": [] } ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Microsoft Open Source Code of Conduct This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). Resources: - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns ================================================ FILE: CONTRIBUTING.md ================================================ ## Contributing This project welcomes contributions and suggestions. Before you get started, you should read the [readme](README.md) and [the design and architecture notes](docs/Design_and_Architecture.md), which describe design & architecture goals of the code and tools & techniques used. - 🤔 **CONSIDER** adding a unit test if your PR resolves an issue. - ✅ **DO** check open PR's to avoid duplicates. - ✅ **DO** keep pull requests small so they can be easily reviewed. - ✅ **DO** build locally before pushing. - ✅ **DO** make sure tests pass. - ✅ **DO** make sure any new changes are documented. - ✅ **DO** make sure not to introduce any compiler warnings. - ❌**AVOID** breaking the continuous integration build. - ❌**AVOID** making significant changes to the overall architecture. ### Creating a Pull Request All pull requests should have an accompanying issue. Create one if there is not one matching your code. The code will be checked by continuous integration. Once this CI passes, the code will be reviewed, ideally approved, then merged. ### CLA Open source contributions require you to agree to a standard Microsoft Contributor License Agreement (CLA) declaring that you grant us the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. When you submit a pull request, a CLA bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repos using our CLA. ### Code of Conduct This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) Microsoft Corporation. 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: Makefile ================================================ .PHONY: all environment openssl-fix install install-gmp install-gmp-mac install-gmp-linux install-gmp-windows install-mkdocs auto-lint validate test test-example bench coverage coverage-html coverage-xml coverage-erase fetch-sample-data CODE_COVERAGE ?= 90 OS ?= $(shell python3 -c 'import platform; print(platform.system())') ifeq ($(OS), Linux) PKG_MGR ?= $(shell python3 -c 'import subprocess as sub; print(next(filter(None, (sub.getstatusoutput(f"command -v {pm}")[0] == 0 and pm for pm in ["apt-get", "pacman"])), "undefined"))') endif SAMPLE_BALLOT_COUNT ?= 5 SAMPLE_BALLOT_SPOIL_RATE ?= 50 all: environment install build validate auto-lint coverage environment: @echo 🔧 ENVIRONMENT SETUP make install-gmp python3 -m pip install -U pip pip3 install 'poetry==2.2.1' poetry config virtualenvs.in-project true printf "Cython<3\n" > /tmp/pip-constraints.txt PIP_CONSTRAINT=/tmp/pip-constraints.txt poetry install poetry run pip install 'setuptools<83' @echo 🚨 Be sure to add poetry to PATH make fetch-sample-data install: @echo 🔧 INSTALL poetry install poetry run pip install 'setuptools<83' build: @echo 🔨 BUILD poetry build poetry install poetry run pip install 'setuptools<83' openssl-fix: export LDFLAGS=-L/usr/local/opt/openssl/lib export CPPFLAGS=-I/usr/local/opt/openssl/include install-gmp: @echo 📦 Install gmp @echo Operating System identified as $(OS) ifeq ($(OS), Linux) make install-gmp-linux endif ifeq ($(OS), Darwin) make install-gmp-mac endif install-gmp-mac: @echo 🍎 MACOS INSTALL brew install gmp || true brew install mpfr || true brew install libmpc || true install-gmp-linux: @echo 🐧 LINUX INSTALL ifeq ($(PKG_MGR), apt-get) sudo apt-get update sudo apt-get install libgmp-dev sudo apt-get install libmpfr-dev sudo apt-get install libmpc-dev else ifeq ($(PKG_MGR), pacman) sudo pacman -S gmp else ifeq ($(PKG_MGR), undefined) @echo "We could not install GMP automatically for your Linux distribution. Please, install GMP manually." endif lint: @echo 💚 LINT @echo 1.Pylint make pylint @echo 2.Black Formatting make blackcheck @echo 3.Mypy Static Typing make mypy @echo 4.Package Metadata poetry build poetry run twine check dist/* @echo 5.Documentation poetry run mkdocs build --strict auto-lint: @echo 💚 AUTO LINT @echo Auto-generating __init__ poetry run mkinit src/electionguard --write --black poetry run mkinit src/electionguard_tools --write --recursive --black poetry run mkinit src/electionguard_verify --write --black poetry run mkinit src/electionguard_cli --write --recursive --black poetry run mkinit src/electionguard_gui --write --recursive --black @echo Reformatting using Black make blackformat make lint pylint: poetry run pylint --extension-pkg-allow-list=dependency_injector ./src ./tests blackformat: poetry run black . blackcheck: poetry run black --check . mypy: poetry run mypy src/electionguard src/electionguard_tools src/electionguard_cli src/electionguard_gui stubs validate: @echo ✅ VALIDATE @poetry run python3 -c 'import electionguard; print(electionguard.__package__ + " successfully imported")' # Test unit-tests: @echo ✅ UNIT TESTS poetry run pytest tests/unit property-tests: @echo ✅ PROPERTY TESTS poetry run pytest tests/property integration-tests: @echo ✅ INTEGRATION TESTS poetry run pytest tests/integration test: @echo ✅ ALL TESTS make unit-tests make property-tests make integration-tests test-example: @echo ✅ TEST Example poetry run python3 -m pytest -s tests/integration/test_end_to_end_election.py test-integration: @echo ✅ INTEGRATION TESTS poetry run pytest tests/integration # Coverage coverage: @echo ✅ COVERAGE poetry run coverage run -m pytest poetry run coverage report --fail-under=$(CODE_COVERAGE) coverage-html: poetry run coverage html -d coverage coverage-xml: poetry run coverage xml coverage-erase: @poetry run coverage erase # Benchmark bench: @echo 📊 BENCHMARKS poetry run python3 -s tests/bench/bench_chaum_pedersen.py # Documentation install-mkdocs: pip install mkdocs pip install mkdocs-jupyter docs-serve: poetry run mkdocs serve docs-build: poetry run mkdocs build docs-deploy: @echo 🚀 DEPLOY to Github Pages poetry run mkdocs gh-deploy --force docs-deploy-ci: @echo 🚀 DEPLOY to Github Pages poetry run mkdocs gh-deploy --force dependency-graph: poetry run pydeps --noshow --max-bacon 2 -o dependency-graph.svg src/electionguard dependency-graph-ci: sudo apt install graphviz poetry run pydeps --noshow --max-bacon 2 -o dependency-graph.svg src/electionguard # Sample Data fetch-sample-data: @echo ⬇️ FETCH Sample Data ifeq ($(OS), Windows) choco install wget choco install unzip endif wget -O sample-data.zip https://github.com/microsoft/electionguard/releases/download/v1.0/sample-data.zip unzip -o sample-data.zip generate-sample-data: @echo 🔁 GENERATE Sample Data poetry run python3 src/electionguard_tools/scripts/sample_generator.py -m "hamilton-general" -n $(SAMPLE_BALLOT_COUNT) -s $(SAMPLE_BALLOT_SPOIL_RATE) # Publish publish: poetry publish publish-ci: @echo 🚀 PUBLISH poetry publish --username __token__ --password $(PYPI_TOKEN) publish-test: poetry publish --repository testpypi publish-test-ci: @echo 🚀 PUBLISH TEST poetry publish --repository testpypi --username __token__ --password $(TEST_PYPI_TOKEN) # Release release-zip-ci: @echo 📁 ZIP RELEASE ARTIFACTS mv dist electionguard mv dependency-graph.svg electionguard zip -r electionguard.zip electionguard release-notes: @echo 📝 GENERATE RELEASE NOTES export MILESTONE_NUM=$(cat ${GITHUB_EVENT_PATH} | jq '.milestone.number') export MILESTONE_URL=$(cat ${GITHUB_EVENT_PATH} | jq '.milestone.url') export MILESTONE_TITLE=$(cat ${GITHUB_EVENT_PATH} | jq '.milestone.title') export MILESTONE_DESCRIPTION=$(cat ${GITHUB_EVENT_PATH} | jq '.milestone.description') touch release_notes.md echo "# ${MILESTONE_TITLE}" >> release_notes.md echo "${MILESTONE_DESCRIPTION}" >> release_notes.md echo -en "\n" >> release_notes.md echo "## Issues" >> release_notes.md curl "${GITHUB_API_URL}/${GITHUB_REPOSITORY}/issues?milestone=${MILESTONE_NUM}&state=all" | jq '.[].title' | while read i; do echo "[$i](${MILESTONE_URL})" >> release_notes.md; done egui: ifeq "${EG_DB_PASSWORD}" "" @echo "Set the EG_DB_PASSWORD environment variable" exit 1 endif poetry run egui start-db: ifeq "${EG_DB_PASSWORD}" "" @echo "Set the EG_DB_PASSWORD environment variable" exit 1 endif docker compose --env-file ./.env -f src/electionguard_db/docker-compose.db.yml up -d stop-db: docker compose --env-file ./.env -f src/electionguard_db/docker-compose.db.yml down build-egui: docker build -t egui -f ./src/electionguard_gui/Dockerfile . start-egui: build-egui ifeq "${EG_DB_PASSWORD}" "" @echo "Set the EG_DB_PASSWORD environment variable" exit 1 endif docker compose --env-file ./.env -f src/electionguard_gui/docker-compose.yml up -d stop-egui: docker compose --env-file ./.env -f src/electionguard_gui/docker-compose.yml down eg-e2e-simple-election: poetry run eg e2e --guardian-count=2 --quorum=2 --manifest=data/election_manifest_simple.json --ballots=data/plaintext_ballots_simple.json --spoil-id=25a7111b-4334-425a-87c1-f7a49f42b3a2 --output-record="./election_record.zip" eg-setup-simple-election: poetry run eg setup --guardian-count=2 --quorum=2 --manifest=data/election_manifest_simple.json --package-dir=../data/out/public_encryption_package --keys-dir=../data/out/test_data_private_guardian_data ================================================ FILE: README.md ================================================ ![Microsoft Defending Democracy Program: ElectionGuard Python][banner image] # 🗳 ElectionGuard Python [![ElectionGuard Specification 0.95.0](https://img.shields.io/badge/🗳%20ElectionGuard%20Specification-0.95.0-green)](https://www.electionguard.vote) ![Github Package Action](https://github.com/microsoft/electionguard-python/workflows/Release%20Build/badge.svg) [![](https://img.shields.io/pypi/v/electionguard)](https://pypi.org/project/electionguard/) [![](https://img.shields.io/pypi/dm/electionguard)](https://pypi.org/project/electionguard/) [![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/microsoft/electionguard-python.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/microsoft/electionguard-python/context:python) [![Total alerts](https://img.shields.io/lgtm/alerts/g/microsoft/electionguard-python.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/microsoft/electionguard-python/alerts/) [![Documentation Status](https://readthedocs.org/projects/electionguard-python/badge/?version=latest)](https://electionguard-python.readthedocs.io) [![license](https://img.shields.io/github/license/microsoft/electionguard)](https://github.com/microsoft/electionguard-python/blob/main/LICENSE) This repository is a "reference implementation" of ElectionGuard written in Python 3. This implementation can be used to conduct End-to-End Verifiable Elections as well as privacy-enhanced risk-limiting audits. Components of this library can also be used to construct "Verifiers" to validate the results of an ElectionGuard election. ## 📁 In This Repository | File/folder | Description | | ------------------------------------------------------- | ---------------------------------------------- | | [docs](/docs) | Documentation for using the library. | | [src/electionguard](/src/electionguard) | ElectionGuard library. | | [src/electionguard_tools](/src/electionguard_tools) | Tools for testing and sample data. | | [src/electionguard_verifier](/src/electionguard_verify) | Verifier to validate the validity of a ballot. | | [stubs](/stubs) | Type annotations for external libraries. | | [tests](/tests) | Tests to exercise this codebase. | | [CONTRIBUTING.md](/CONTRIBUTING.md) | Guidelines for contributing. | | [README.md](/README.md) | This README file. | | [LICENSE](/LICENSE) | The license for ElectionGuard-Python. | | [data](/data) | Sample election data. | ## ❓ What Is ElectionGuard? ElectionGuard is an open source software development kit (SDK) that makes voting more secure, transparent and accessible. The ElectionGuard SDK leverages homomorphic encryption to ensure that votes recorded by electronic systems of any type remain encrypted, secure, and secret. Meanwhile, ElectionGuard also allows verifiable and accurate tallying of ballots by any 3rd party organization without compromising secrecy or security. Learn More in the [ElectionGuard Repository](https://github.com/microsoft/electionguard) ## 🦸 How Can I use ElectionGuard? ElectionGuard supports a variety of use cases. The Primary use case is to generate verifiable end-to-end (E2E) encrypted elections. The ElectionGuard process can also be used for other use cases such as privacy enhanced risk-limiting audits (RLAs). ## 💻 Requirements - [Python 3.9+](https://www.python.org/downloads/) is <ins>**required**</ins> to develop this SDK. If developer uses multiple versions of python, [pyenv](https://github.com/pyenv/pyenv) is suggested to assist version management. - [GNU Make](https://www.gnu.org/software/make/manual/make.html) is used to simplify the commands and GitHub Actions. This approach is recommended to simplify the command line experience. This is built in for MacOS and Linux. For Windows, setup is simpler with [Chocolatey](https://chocolatey.org/install) and installing the provided [make package](https://chocolatey.org/packages/make). The other Windows option is [manually installing make](http://gnuwin32.sourceforge.net/packages/make.htm). - [Gmpy2](https://gmpy2.readthedocs.io/en/latest/) is used for [Arbitrary-precision arithmetic](https://en.wikipedia.org/wiki/Arbitrary-precision_arithmetic) which has its own [installation requirements (native C libraries)](https://gmpy2.readthedocs.io/en/latest/intro.html#installation) on Linux and MacOS. **⚠️ Note:** _This is not required for Windows since the gmpy2 precompiled libraries are provided._ - [poetry 2.2.1](https://python-poetry.org/) is used to configure the python environment. Installation instructions can be found [here](https://python-poetry.org/docs/#installation). ## 🚀 Quick Start Using [**make**](https://www.gnu.org/software/make/manual/make.html), the entire [GitHub Action workflow][pull request workflow] can be run with one command: ``` make ``` The unit and integration tests can also be run with make: ``` make test ``` A complete end-to-end election example can be run independently by executing: ``` make test-example ``` For more detailed build and run options, see the [documentation][build and run]. ## 📄 Documentation Overviews: - [GitHub Pages](https://microsoft.github.io/electionguard-python/) - [Read the Docs](https://electionguard-python.readthedocs.io/) Sections: - [Design and Architecture] - [Build and Run] - [Project Workflow] - [Election Manifest] Step-by-Step Process: 0. [Configure Election] 1. [Key Ceremony] 2. [Encrypt Ballots] 3. [Cast and Spoil] 4. [Decrypt Tally] 5. [Publish and Verify] ## Contributing This project encourages community contributions for development, testing, documentation, code review, and performance analysis, etc. For more information on how to contribute, see [the contribution guidelines][contributing] ### Code of Conduct This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. ### Reporting Issues Please report any bugs, feature requests, or enhancements using the [GitHub Issue Tracker](https://github.com/microsoft/electionguard-python/issues). Please do not report any security vulnerabilities using the Issue Tracker. Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). See the [Security Documentation][security] for more information. ### Have Questions? Electionguard would love for you to ask questions out in the open using GitHub Issues. If you really want to email the ElectionGuard team, reach out at electionguard@microsoft.com. ## License This repository is licensed under the [MIT License] ## Thanks! 🎉 A huge thank you to those who helped to contribute to this project so far, including: **[Josh Benaloh _(Microsoft)_](https://www.microsoft.com/en-us/research/people/benaloh/)** <a href="https://www.microsoft.com/en-us/research/people/benaloh/"><img src="https://www.microsoft.com/en-us/research/wp-content/uploads/2016/09/avatar_user__1473484671-180x180.jpg" title="Josh Benaloh" width="80" height="80"></a> **[Keith Fung](https://github.com/keithrfung) [_(InfernoRed Technology)_](https://infernored.com/)** <a href="https://github.com/keithrfung"><img src="https://avatars2.githubusercontent.com/u/10125297?v=4" title="keithrfung" width="80" height="80"></a> **[Matt Wilhelm](https://github.com/AddressXception) [_(InfernoRed Technology)_](https://infernored.com/)** <a href="https://github.com/AddressXception"><img src="https://avatars0.githubusercontent.com/u/6232853?s=460&u=8fec95386acad6109ad71a2aad2d097b607ebd6a&v=4" title="AddressXception" width="80" height="80"></a> **[Dan S. Wallach](https://www.cs.rice.edu/~dwallach/) [_(Rice University)_](https://www.rice.edu/)** <a href="https://www.cs.rice.edu/~dwallach/"><img src="https://avatars2.githubusercontent.com/u/743029?v=4" title="danwallach" width="80" height="80"></a> <!-- Links --> [banner image]: https://raw.githubusercontent.com/microsoft/electionguard-python/main/images/electionguard-banner.svg [pull request workflow]: https://github.com/microsoft/electionguard-python/blob/main/.github/workflows/pull_request.yml [contributing]: https://github.com/microsoft/electionguard-python/blob/main/CONTRIBUTING.md [security]: https://github.com/microsoft/electionguard-python/blob/main/SECURITY.md [design and architecture]: https://github.com/microsoft/electionguard-python/blob/main/docs/Design_and_Architecture.md [build and run]: https://github.com/microsoft/electionguard-python/blob/main/docs/Build_and_Run.md [project workflow]: https://github.com/microsoft/electionguard-python/blob/main/docs/Project_Workflow.md [election manifest]: https://github.com/microsoft/electionguard-python/blob/main/docs/Election_Manifest.md [configure election]: https://github.com/microsoft/electionguard-python/blob/main/docs/0_Configure_Election.md [key ceremony]: https://github.com/microsoft/electionguard-python/blob/main/docs/1_Key_Ceremony.md [encrypt ballots]: https://github.com/microsoft/electionguard-python/blob/main/docs/2_Encrypt_Ballots.md [cast and spoil]: https://github.com/microsoft/electionguard-python/blob/main/docs/3_Cast_and_Spoil.md [decrypt tally]: https://github.com/microsoft/electionguard-python/blob/main/docs/4_Decrypt_Tally.md [publish and verify]: https://github.com/microsoft/electionguard-python/blob/main/docs/5_Publish_and_Verify.md [mit license]: https://github.com/microsoft/electionguard-python/blob/main/LICENSE ================================================ FILE: SECURITY.md ================================================ <!-- BEGIN MICROSOFT SECURITY.MD V0.0.3 BLOCK --> ## Security Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). If you believe you have found a security vulnerability in any Microsoft-owned repository that meets Microsoft's [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)) of a security vulnerability, please report it to us as described below. ## Reporting Security Issues **Please do not report security vulnerabilities through public GitHub issues.** Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) * Full paths of source file(s) related to the manifestation of the issue * The location of the affected source code (tag/branch/commit or direct URL) * Any special configuration required to reproduce the issue * Step-by-step instructions to reproduce the issue * Proof-of-concept or exploit code (if possible) * Impact of the issue, including how an attacker might exploit the issue This information will help us triage your report more quickly. If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. ## Preferred Languages We prefer all communications to be in English. ## Policy Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). <!-- END MICROSOFT SECURITY.MD BLOCK --> ================================================ FILE: data/ballot_in_simple.json ================================================ { "object_id": "some-external-id-string-123", "style_id": "jefferson-county-ballot-style", "contests": [ { "object_id": "justice-supreme-court", "sequence_order": 0, "ballot_selections": [ { "object_id": "john-adams-selection", "sequence_order": 0, "vote": 1 }, { "object_id": "write-in-selection", "sequence_order": 3, "vote": 1, "extended_data": { "value": "Susan B. Anthony", "length": 16 } } ] } ] } ================================================ FILE: data/election_manifest_simple.json ================================================ { "spec_version": "v0.95", "geopolitical_units": [ { "object_id": "jefferson-county", "name": "Jefferson County", "type": "county", "contact_information": { "address_line": ["1234 Samuel Adams Way", "Jefferson, Hamilton 999999"], "name": "Jefferson County Clerk", "email": [ { "annotation": "inquiries", "value": "inquiries@jefferson.hamilton.state.gov" } ], "phone": [ { "annotation": "domestic", "value": "123-456-7890" } ] } }, { "object_id": "harrison-township", "name": "Harrison Township", "type": "township", "contact_information": { "address_line": ["1234 Thorton Drive", "Harrison, Hamilton 999999"], "name": "Harrison Town Hall", "email": [ { "annotation": "inquiries", "value": "inquiries@harrison.hamilton.state.gov" } ], "phone": [ { "annotation": "domestic", "value": "123-456-7890" } ] } }, { "object_id": "harrison-township-precinct-east", "name": "Harrison Township Precinct", "type": "township", "contact_information": { "address_line": ["1234 Thorton Drive", "Harrison, Hamilton 999999"], "name": "Harrison Town Hall", "email": [ { "annotation": "inquiries", "value": "inquiries@harrison.hamilton.state.gov" } ], "phone": [ { "annotation": "domestic", "value": "123-456-7890" } ] } }, { "object_id": "rutledge-elementary", "name": "Rutledge Elementary School district", "type": "school", "contact_information": { "address_line": ["1234 Wolcott Parkway", "Harrison, Hamilton 999999"], "name": "Rutledge Elementary School", "email": [ { "annotation": "inquiries", "value": "inquiries@harrison.hamilton.state.gov" } ], "phone": [ { "annotation": "domestic", "value": "123-456-7890" } ] } } ], "parties": [ { "object_id": "whig", "abbreviation": "WHI", "color": "AAAAAA", "logo_uri": "http://some/path/to/whig.svg", "name": { "text": [ { "value": "Whig Party", "language": "en" } ] } }, { "object_id": "federalist", "abbreviation": "FED", "color": "CCCCCC", "logo_uri": "http://some/path/to/federalist.svg", "name": { "text": [ { "value": "Federalist Party", "language": "en" } ] } }, { "object_id": "democratic-republican", "abbreviation": "DEMREP", "color": "EEEEEE", "logo_uri": "http://some/path/to/democratic-repulbican.svg", "name": { "text": [ { "value": "Democratic Republican Party", "language": "en" } ] } } ], "candidates": [ { "object_id": "benjamin-franklin", "name": { "text": [ { "value": "Benjamin Franklin", "language": "en" } ] }, "party_id": "whig" }, { "object_id": "john-adams", "name": { "text": [ { "value": "John Adams", "language": "en" } ] }, "party_id": "federalist" }, { "object_id": "john-hancock", "name": { "text": [ { "value": "John Hancock", "language": "en" } ] }, "party_id": "democratic-republican" }, { "object_id": "write-in", "name": { "text": [ { "value": "Write In Candidate", "language": "en" }, { "value": "Escribir en la candidata", "language": "es" } ] }, "is_write_in": true }, { "object_id": "referendum-pineapple-affirmative", "name": { "text": [ { "value": "Pineapple should be banned on pizza", "language": "en" } ] } }, { "object_id": "referendum-pineapple-negative", "name": { "text": [ { "value": "Pineapple should not be banned on pizza", "language": "en" } ] } } ], "contests": [ { "object_id": "justice-supreme-court", "sequence_order": 1, "ballot_selections": [ { "object_id": "john-adams-selection", "sequence_order": 1, "candidate_id": "john-adams" }, { "object_id": "benjamin-franklin-selection", "sequence_order": 2, "candidate_id": "benjamin-franklin" }, { "object_id": "john-hancock-selection", "sequence_order": 3, "candidate_id": "john-hancock" }, { "object_id": "write-in-selection", "sequence_order": 4, "candidate_id": "write-in" } ], "ballot_title": { "text": [ { "value": "Justice of the Supreme Court", "language": "en" }, { "value": "Juez de la corte suprema", "language": "es" } ] }, "ballot_subtitle": { "text": [ { "value": "Please choose up to two candidates", "language": "en" }, { "value": "Uno", "language": "es" } ] }, "vote_variation": "n_of_m", "electoral_district_id": "jefferson-county", "name": "Justice of the Supreme Court", "number_elected": 2, "votes_allowed": 2 }, { "object_id": "referendum-pineapple", "sequence_order": 2, "ballot_selections": [ { "object_id": "referendum-pineapple-affirmative-selection", "sequence_order": 1, "candidate_id": "referendum-pineapple-affirmative" }, { "object_id": "referendum-pineapple-negative-selection", "sequence_order": 2, "candidate_id": "referendum-pineapple-negative" } ], "ballot_title": { "text": [ { "value": "Should pineapple be banned on pizza?", "language": "en" }, { "value": "¿Debería prohibirse la piña en la pizza?", "language": "es" } ] }, "ballot_subtitle": { "text": [ { "value": "The township considers this issue to be very important", "language": "en" }, { "value": "El municipio considera que esta cuestión es muy importante", "language": "es" } ] }, "vote_variation": "one_of_m", "electoral_district_id": "harrison-township", "name": "The Pineapple Question", "number_elected": 1, "votes_allowed": 1 } ], "ballot_styles": [ { "object_id": "jefferson-county-ballot-style", "geopolitical_unit_ids": ["jefferson-county"] }, { "object_id": "harrison-township-ballot-style", "geopolitical_unit_ids": ["jefferson-county", "harrison-township"] }, { "object_id": "harrison-township-precinct-east-ballot-style", "geopolitical_unit_ids": [ "jefferson-county", "harrison-township", "harrison-township-precinct-east", "rutledge-elementary" ] }, { "object_id": "rutledge-elementary-ballot-style", "geopolitical_unit_ids": [ "jefferson-county", "harrison-township", "rutledge-elementary" ] } ], "name": { "text": [ { "value": "Jefferson County Spring Primary", "language": "en" }, { "value": "Primaria de primavera del condado de Jefferson", "language": "es" } ] }, "contact_information": { "address_line": ["1234 Paul Revere Run", "Jefferson, Hamilton 999999"], "name": "Hamilton State Election Commission", "email": [ { "annotation": "press", "value": "inquiries@hamilton.state.gov" }, { "annotation": "federal", "value": "commissioner@hamilton.state.gov" } ], "phone": [ { "annotation": "domestic", "value": "123-456-7890" }, { "annotation": "international", "value": "+1-123-456-7890" } ] }, "start_date": "2020-03-01T08:00:00-05:00", "end_date": "2020-03-01T20:00:00-05:00", "election_scope_id": "jefferson-county-primary", "type": "primary" } ================================================ FILE: data/manifest-full.json ================================================ { "election_scope_id": "jefferson-county-primary", "spec_version": "1.0", "type": "primary", "start_date": "2020-03-01T08:00:00-05:00", "end_date": "2020-03-01T20:00:00-05:00", "geopolitical_units": [ { "object_id": "jefferson-county", "name": "Jefferson County", "type": "county", "contact_information": { "address_line": [ "1234 Samuel Adams Way", "Jefferson, Hamilton 999999" ], "email": [ { "annotation": "inquiries", "value": "inquiries@jefferson.hamilton.state.gov" } ], "phone": [ { "annotation": "domestic", "value": "123-456-7890" } ], "name": "Jefferson County Clerk" } }, { "object_id": "harrison-township", "name": "Harrison Township", "type": "township", "contact_information": { "address_line": [ "1234 Thorton Drive", "Harrison, Hamilton 999999" ], "email": [ { "annotation": "inquiries", "value": "inquiries@harrison.hamilton.state.gov" } ], "phone": [ { "annotation": "domestic", "value": "123-456-7890" } ], "name": "Harrison Town Hall" } }, { "object_id": "harrison-township-precinct-east", "name": "Harrison Township Precinct", "type": "township", "contact_information": { "address_line": [ "1234 Thorton Drive", "Harrison, Hamilton 999999" ], "email": [ { "annotation": "inquiries", "value": "inquiries@harrison.hamilton.state.gov" } ], "phone": [ { "annotation": "domestic", "value": "123-456-7890" } ], "name": "Harrison Town Hall" } }, { "object_id": "rutledge-elementary", "name": "Rutledge Elementary School district", "type": "school", "contact_information": { "address_line": [ "1234 Wolcott Parkway", "Harrison, Hamilton 999999" ], "email": [ { "annotation": "inquiries", "value": "inquiries@harrison.hamilton.state.gov" } ], "phone": [ { "annotation": "domestic", "value": "123-456-7890" } ], "name": "Rutledge Elementary School" } } ], "parties": [ { "object_id": "whig", "name": { "text": [ { "value": "Whig Party", "language": "en" } ] }, "abbreviation": "WHI", "color": "AAAAAA", "logo_uri": "http://some/path/to/whig.svg" }, { "object_id": "federalist", "name": { "text": [ { "value": "Federalist Party", "language": "en" } ] }, "abbreviation": "FED", "color": "CCCCCC", "logo_uri": "http://some/path/to/federalist.svg" }, { "object_id": "democratic-republican", "name": { "text": [ { "value": "Democratic Republican Party", "language": "en" } ] }, "abbreviation": "DEMREP", "color": "EEEEEE", "logo_uri": "http://some/path/to/democratic-repulbican.svg" } ], "candidates": [ { "object_id": "benjamin-franklin", "name": { "text": [ { "value": "Benjamin Franklin", "language": "en" } ] }, "party_id": "whig", "image_uri": null, "is_write_in": null }, { "object_id": "john-adams", "name": { "text": [ { "value": "John Adams", "language": "en" } ] }, "party_id": "federalist", "image_uri": null, "is_write_in": null }, { "object_id": "john-hancock", "name": { "text": [ { "value": "John Hancock", "language": "en" } ] }, "party_id": "democratic-republican", "image_uri": null, "is_write_in": null }, { "object_id": "write-in", "name": { "text": [ { "value": "Write In Candidate", "language": "en" }, { "value": "Escribir en la candidata", "language": "es" } ] }, "party_id": null, "image_uri": null, "is_write_in": true }, { "object_id": "referendum-pineapple-affirmative", "name": { "text": [ { "value": "Pineapple should be banned on pizza", "language": "en" } ] }, "party_id": null, "image_uri": null, "is_write_in": null }, { "object_id": "referendum-pineapple-negative", "name": { "text": [ { "value": "Pineapple should not be banned on pizza", "language": "en" } ] }, "party_id": null, "image_uri": null, "is_write_in": null } ], "contests": [ { "object_id": "justice-supreme-court", "sequence_order": 0, "electoral_district_id": "jefferson-county", "vote_variation": "n_of_m", "number_elected": 2, "votes_allowed": 2, "name": "Justice of the Supreme Court", "ballot_selections": [ { "object_id": "john-adams-selection", "sequence_order": 0, "candidate_id": "john-adams" }, { "object_id": "benjamin-franklin-selection", "sequence_order": 1, "candidate_id": "benjamin-franklin" }, { "object_id": "john-hancock-selection", "sequence_order": 2, "candidate_id": "john-hancock" }, { "object_id": "write-in-selection", "sequence_order": 3, "candidate_id": "write-in" } ], "ballot_title": { "text": [ { "value": "Justice of the Supreme Court", "language": "en" }, { "value": "Juez de la corte suprema", "language": "es" } ] }, "ballot_subtitle": { "text": [ { "value": "Please choose up to two candidates", "language": "en" }, { "value": "Uno", "language": "es" } ] } }, { "object_id": "referendum-pineapple", "sequence_order": 1, "electoral_district_id": "harrison-township", "vote_variation": "one_of_m", "number_elected": 1, "votes_allowed": 1, "name": "The Pineapple Question", "ballot_selections": [ { "object_id": "referendum-pineapple-affirmative-selection", "sequence_order": 0, "candidate_id": "referendum-pineapple-affirmative" }, { "object_id": "referendum-pineapple-negative-selection", "sequence_order": 1, "candidate_id": "referendum-pineapple-negative" } ], "ballot_title": { "text": [ { "value": "Should pineapple be banned on pizza?", "language": "en" }, { "value": "\u00bfDeber\u00eda prohibirse la pi\u00f1a en la pizza?", "language": "es" } ] }, "ballot_subtitle": { "text": [ { "value": "The township considers this issue to be very important", "language": "en" }, { "value": "El municipio considera que esta cuesti\u00f3n es muy importante", "language": "es" } ] } } ], "ballot_styles": [ { "object_id": "jefferson-county-ballot-style", "geopolitical_unit_ids": [ "jefferson-county" ], "party_ids": null, "image_uri": null }, { "object_id": "harrison-township-ballot-style", "geopolitical_unit_ids": [ "jefferson-county", "harrison-township" ], "party_ids": null, "image_uri": null }, { "object_id": "harrison-township-precinct-east-ballot-style", "geopolitical_unit_ids": [ "jefferson-county", "harrison-township", "harrison-township-precinct-east", "rutledge-elementary" ], "party_ids": null, "image_uri": null }, { "object_id": "rutledge-elementary-ballot-style", "geopolitical_unit_ids": [ "jefferson-county", "harrison-township", "rutledge-elementary" ], "party_ids": null, "image_uri": null } ], "name": { "text": [ { "value": "Jefferson County Spring Primary", "language": "en" }, { "value": "Primaria de primavera del condado de Jefferson", "language": "es" } ] }, "contact_information": { "address_line": [ "1234 Paul Revere Run", "Jefferson, Hamilton 999999" ], "email": [ { "annotation": "press", "value": "inquiries@hamilton.state.gov" }, { "annotation": "federal", "value": "commissioner@hamilton.state.gov" } ], "phone": [ { "annotation": "domestic", "value": "123-456-7890" }, { "annotation": "international", "value": "+1-123-456-7890" } ], "name": "Hamilton State Election Commission" } } ================================================ FILE: data/manifest-hamilton-general.json ================================================ { "election_scope_id": "hamilton-county-general-election", "spec_version": "1.0", "type": "general", "start_date": "2020-03-01T08:00:00-05:00", "end_date": "2020-03-01T20:00:00-05:00", "geopolitical_units": [ { "object_id": "hamilton-county", "name": "Hamilton County", "type": "county", "contact_information": { "address_line": [ "1234 Samuel Adams Way", "Hamilton, Ozark 99999" ], "email": [ { "annotation": "inquiries", "value": "inquiries@hamiltoncounty.gov" } ], "phone": [ { "annotation": "domestic", "value": "123-456-7890" } ], "name": "Hamilton County Clerk" } }, { "object_id": "congress-district-5", "name": "Congressional District 5", "type": "congressional", "contact_information": { "address_line": [ "1234 Somerville Gateway", "Medford, Ozark 999999" ], "email": [ { "annotation": "inquiries", "value": "inquiries@congressional-district-5.gov" } ], "phone": [ { "annotation": "domestic", "value": "123-456-7890" } ], "name": "Medford Town Hall" } }, { "object_id": "congress-district-7", "name": "Congressional District 7", "type": "congressional", "contact_information": { "address_line": [ "1234 Somerville Gateway", "Arlington, Ozark 999999" ], "email": [ { "annotation": "inquiries", "value": "inquiries@congressional-district-7.gov" } ], "phone": [ { "annotation": "domestic", "value": "123-456-7890" } ], "name": "Arlington Town Hall" } }, { "object_id": "lacroix-township-precinct-1", "name": "LaCroix Township Precinct 1", "type": "precinct", "contact_information": { "address_line": [ "1234 Thorton Drive", "LaCroix, Ozark 99999" ], "email": [ { "annotation": "inquiries", "value": "inquiries@lacrox.gov" } ], "phone": [ { "annotation": "domestic", "value": "123-456-7890" } ], "name": "LaCroix Town Hall" } }, { "object_id": "lacroix-exeter-utility-district", "name": "Exeter Utility District", "type": "utility", "contact_information": { "address_line": [ "1234 Watt Drive", "LaCroix, Ozark 99999" ], "email": [ { "annotation": "inquiries", "value": "inquiries@exeter-utility.com" } ], "phone": [ { "annotation": "domestic", "value": "123-456-7890" } ], "name": "Exeter Utility District coordinator" } }, { "object_id": "arlington-township-precinct-1", "name": "Arlington Township Precinct 1", "type": "precinct", "contact_information": { "address_line": [ "1234 Pahk Avenue", "Arlinton, Ozark 99999" ], "email": [ { "annotation": "inquiries", "value": "inquiries@arlington-township.gov" } ], "phone": [ { "annotation": "domestic", "value": "123-456-7890" } ], "name": "Arlington Town Hall" } }, { "object_id": "pismo-beach-school-district-precinct-1", "name": "Pismo Beach School District Precinct 1", "type": "school", "contact_information": { "address_line": [ "1234 Pismo Beach Elementary", "Arlington, Ozark 99999" ], "email": [ { "annotation": "inquiries", "value": "inquiries@pismo-beach-school.edu" } ], "phone": [ { "annotation": "domestic", "value": "123-456-7890" } ], "name": "Pismo Beah Elementary" } }, { "object_id": "somerset-school-district-precinct-1", "name": "Somerset School District", "type": "school", "contact_information": { "address_line": [ "1234 Somerset Avenue", "Arlinton, Ozark 99999" ], "email": [ { "annotation": "inquiries", "value": "inquiries@somerset-elementary.edu" } ], "phone": [ { "annotation": "domestic", "value": "123-456-7890" } ], "name": "Someset Elementary" } }, { "object_id": "harris-township", "name": "Harris Township", "type": "township", "contact_information": { "address_line": [ "1234 Pahk Avenue", "Harris, Ozark 99999" ], "email": [ { "annotation": "inquiries", "value": "inquiries@harris-township.gov" } ], "phone": [ { "annotation": "domestic", "value": "123-456-7890" } ], "name": "harris Town Hall" } } ], "parties": [ { "object_id": "whig", "name": { "text": [ { "value": "Whig Party", "language": "en" } ] }, "abbreviation": "WHI", "color": "AAAAAA", "logo_uri": "http://some/path/to/whig.svg" }, { "object_id": "federalist", "name": { "text": [ { "value": "Federalist Party", "language": "en" } ] }, "abbreviation": "FED", "color": "BBBBBB", "logo_uri": "http://some/path/to/federalist.svg" }, { "object_id": "peoples", "name": { "text": [ { "value": "People's Party", "language": "en" } ] }, "abbreviation": "PPL", "color": "CCCCCC", "logo_uri": "http://some/path/to/people-s.svg" }, { "object_id": "liberty", "name": { "text": [ { "value": "Liberty Party", "language": "en" } ] }, "abbreviation": "LIB", "color": "DDDDDD", "logo_uri": "http://some/path/to/liberty.svg" }, { "object_id": "constitution", "name": { "text": [ { "value": "Constitution Party", "language": "en" } ] }, "abbreviation": "CONST", "color": "EEEEEE", "logo_uri": "http://some/path/to/democratic-repulbican.svg" }, { "object_id": "labor", "name": { "text": [ { "value": "Labor Party", "language": "en" } ] }, "abbreviation": "LBR", "color": "FFFFFF", "logo_uri": "http://some/path/to/laobr.svg" }, { "object_id": "independent", "name": { "text": [ { "value": "Independent", "language": "en" } ] }, "abbreviation": "IND", "color": "000000", "logo_uri": "http://some/path/to/independent.svg" } ], "candidates": [ { "object_id": "barchi-hallaren", "name": { "text": [ { "value": "Joseph Barchi and Joseph Hallaren", "language": "en" } ] }, "party_id": "whig", "image_uri": null, "is_write_in": null }, { "object_id": "cramer-vuocolo", "name": { "text": [ { "value": "Adam Cramer and Greg Vuocolo", "language": "en" } ] }, "party_id": "federalist", "image_uri": null, "is_write_in": null }, { "object_id": "court-blumhardt", "name": { "text": [ { "value": "Daniel Court and Amy Blumhardt", "language": "en" } ] }, "party_id": "peoples", "image_uri": null, "is_write_in": null }, { "object_id": "boone-lian", "name": { "text": [ { "value": "Alvin Boone and James Lian", "language": "en" } ] }, "party_id": "liberty", "image_uri": null, "is_write_in": null }, { "object_id": "hildebrand-garritty", "name": { "text": [ { "value": "Ashley Hildebrand-McDougall and James Garritty", "language": "en" } ] }, "party_id": "constitution", "image_uri": null, "is_write_in": null }, { "object_id": "patterson-lariviere", "name": { "text": [ { "value": "Martin Patterson and Clay Lariviere", "language": "en" } ] }, "party_id": "labor", "image_uri": null, "is_write_in": null }, { "object_id": "franz", "name": { "text": [ { "value": "Charlene Franz", "language": "en" } ] }, "party_id": "federalist", "image_uri": null, "is_write_in": null }, { "object_id": "harris", "name": { "text": [ { "value": "Gerald Harris", "language": "en" } ] }, "party_id": "peoples", "image_uri": null, "is_write_in": null }, { "object_id": "bargmann", "name": { "text": [ { "value": "Linda Bargmann", "language": "en" } ] }, "party_id": "constitution", "image_uri": null, "is_write_in": null }, { "object_id": "abcock", "name": { "text": [ { "value": "Barbara Abcock", "language": "en" } ] }, "party_id": "liberty", "image_uri": null, "is_write_in": null }, { "object_id": "steel-loy", "name": { "text": [ { "value": "Carrie Steel-Loy", "language": "en" } ] }, "party_id": "labor", "image_uri": null, "is_write_in": null }, { "object_id": "sharp", "name": { "text": [ { "value": "Frederick Sharp", "language": "en" } ] }, "party_id": "constitution", "image_uri": null, "is_write_in": null }, { "object_id": "wallace", "name": { "text": [ { "value": "Alex Wallace", "language": "en" } ] }, "party_id": "independent", "image_uri": null, "is_write_in": null }, { "object_id": "williams", "name": { "text": [ { "value": "Barbara Williams", "language": "en" } ] }, "party_id": "peoples", "image_uri": null, "is_write_in": null }, { "object_id": "sharp-althea", "name": { "text": [ { "value": "Althea Sharp", "language": "en" } ] }, "party_id": "whig", "image_uri": null, "is_write_in": null }, { "object_id": "alpern", "name": { "text": [ { "value": "Douglas Alpern", "language": "en" } ] }, "party_id": "federalist", "image_uri": null, "is_write_in": null }, { "object_id": "windbeck", "name": { "text": [ { "value": "Ann Windbeck", "language": "en" } ] }, "party_id": "peoples", "image_uri": null, "is_write_in": null }, { "object_id": "greher", "name": { "text": [ { "value": "Mike Greher", "language": "en" } ] }, "party_id": "constitution", "image_uri": null, "is_write_in": null }, { "object_id": "alexander", "name": { "text": [ { "value": "Patricia Alexander", "language": "en" } ] }, "party_id": "whig", "image_uri": null, "is_write_in": null }, { "object_id": "mitchell", "name": { "text": [ { "value": "Kenneth Mitchell", "language": "en" } ] }, "party_id": "federalist", "image_uri": null, "is_write_in": null }, { "object_id": "lee", "name": { "text": [ { "value": "Stan Lee", "language": "en" } ] }, "party_id": "independent", "image_uri": null, "is_write_in": null }, { "object_id": "ash", "name": { "text": [ { "value": "Henry Ash", "language": "en" } ] }, "party_id": "liberty", "image_uri": null, "is_write_in": null }, { "object_id": "kennedy", "name": { "text": [ { "value": "Karen Kennedy", "language": "en" } ] }, "party_id": "independent", "image_uri": null, "is_write_in": null }, { "object_id": "jackson", "name": { "text": [ { "value": "Van Jackson", "language": "en" } ] }, "party_id": "labor", "image_uri": null, "is_write_in": null }, { "object_id": "brown", "name": { "text": [ { "value": "Debbie Brown", "language": "en" } ] }, "party_id": "peoples", "image_uri": null, "is_write_in": null }, { "object_id": "teller", "name": { "text": [ { "value": "Joseph Teller", "language": "en" } ] }, "party_id": "peoples", "image_uri": null, "is_write_in": null }, { "object_id": "ward", "name": { "text": [ { "value": "Greg Ward", "language": "en" } ] }, "party_id": "independent", "image_uri": null, "is_write_in": null }, { "object_id": "murphy", "name": { "text": [ { "value": "Lou Murphy", "language": "en" } ] }, "party_id": "federalist", "image_uri": null, "is_write_in": null }, { "object_id": "newman", "name": { "text": [ { "value": "Jane Newman", "language": "en" } ] }, "party_id": "whig", "image_uri": null, "is_write_in": null }, { "object_id": "callanann", "name": { "text": [ { "value": "Jack Callanann", "language": "en" } ] }, "party_id": "labor", "image_uri": null, "is_write_in": null }, { "object_id": "york", "name": { "text": [ { "value": "Esther York", "language": "en" } ] }, "party_id": "labor", "image_uri": null, "is_write_in": null }, { "object_id": "chandler", "name": { "text": [ { "value": "Glenn Chandler", "language": "en" } ] }, "party_id": "labor", "image_uri": null, "is_write_in": null }, { "object_id": "solis", "name": { "text": [ { "value": "Andrea Solis", "language": "en" } ] }, "party_id": "labor", "image_uri": null, "is_write_in": null }, { "object_id": "keller", "name": { "text": [ { "value": "Amos Keller", "language": "en" } ] }, "party_id": "constitution", "image_uri": null, "is_write_in": null }, { "object_id": "rangel", "name": { "text": [ { "value": "Davitra Rangel", "language": "en" } ] }, "party_id": "peoples", "image_uri": null, "is_write_in": null }, { "object_id": "argent", "name": { "text": [ { "value": "Camille Argent", "language": "en" } ] }, "party_id": "liberty", "image_uri": null, "is_write_in": null }, { "object_id": "witherspoon-smithson", "name": { "text": [ { "value": "Chloe Witherspoon-Smithson", "language": "en" } ] }, "party_id": "independent", "image_uri": null, "is_write_in": null }, { "object_id": "bainbridge", "name": { "text": [ { "value": "Clayton Bainbridge", "language": "en" } ] }, "party_id": "peoples", "image_uri": null, "is_write_in": null }, { "object_id": "hennessey", "name": { "text": [ { "value": "Charlene Hennessey", "language": "en" } ] }, "party_id": "whig", "image_uri": null, "is_write_in": null }, { "object_id": "savoy", "name": { "text": [ { "value": "Eric Savoy", "language": "en" } ] }, "party_id": "labor", "image_uri": null, "is_write_in": null }, { "object_id": "tawa", "name": { "text": [ { "value": "Susan Tawa", "language": "en" } ] }, "party_id": "constitution", "image_uri": null, "is_write_in": null }, { "object_id": "tawa-mary", "name": { "text": [ { "value": "Mary Tawa", "language": "en" } ] }, "party_id": "independent", "image_uri": null, "is_write_in": null }, { "object_id": "altman", "name": { "text": [ { "value": "Valarie Altman", "language": "en" } ] }, "party_id": "peoples", "image_uri": null, "is_write_in": null }, { "object_id": "moore", "name": { "text": [ { "value": "Helen Moore", "language": "en" } ] }, "party_id": null, "image_uri": null, "is_write_in": null }, { "object_id": "white", "name": { "text": [ { "value": "John White", "language": "en" } ] }, "party_id": null, "image_uri": null, "is_write_in": null }, { "object_id": "smallberries", "name": { "text": [ { "value": "John Smallberries", "language": "en" } ] }, "party_id": null, "image_uri": null, "is_write_in": null }, { "object_id": "warfin", "name": { "text": [ { "value": "John Warfin", "language": "en" } ] }, "party_id": null, "image_uri": null, "is_write_in": null }, { "object_id": "norberg", "name": { "text": [ { "value": "Chris Norberg", "language": "en" } ] }, "party_id": null, "image_uri": null, "is_write_in": null }, { "object_id": "parks", "name": { "text": [ { "value": "Abigail Parks", "language": "en" } ] }, "party_id": null, "image_uri": null, "is_write_in": null }, { "object_id": "savannah", "name": { "text": [ { "value": "Harmony Savannah", "language": "en" } ] }, "party_id": null, "image_uri": null, "is_write_in": null }, { "object_id": "summers", "name": { "text": [ { "value": "Buffy Summers", "language": "en" } ] }, "party_id": null, "image_uri": null, "is_write_in": null }, { "object_id": "chase", "name": { "text": [ { "value": "Cordelia Chase", "language": "en" } ] }, "party_id": null, "image_uri": null, "is_write_in": null }, { "object_id": "osborne", "name": { "text": [ { "value": "Daniel Osborne", "language": "en" } ] }, "party_id": null, "image_uri": null, "is_write_in": null }, { "object_id": "rosenberg", "name": { "text": [ { "value": "Willow Rosenberg", "language": "en" } ] }, "party_id": null, "image_uri": null, "is_write_in": null }, { "object_id": "head", "name": { "text": [ { "value": "Anthony Stewart Head", "language": "en" } ] }, "party_id": null, "image_uri": null, "is_write_in": null }, { "object_id": "marsters", "name": { "text": [ { "value": "James Marsters", "language": "en" } ] }, "party_id": null, "image_uri": null, "is_write_in": null }, { "object_id": "write-in-1", "name": { "text": [ { "value": "Write In Candidate", "language": "en" }, { "value": "Escribir en la candidata", "language": "es" } ] }, "party_id": null, "image_uri": null, "is_write_in": true }, { "object_id": "write-in-2", "name": { "text": [ { "value": "Write In Candidate", "language": "en" }, { "value": "Escribir en la candidata", "language": "es" } ] }, "party_id": null, "image_uri": null, "is_write_in": true }, { "object_id": "write-in-3", "name": { "text": [ { "value": "Write In Candidate", "language": "en" }, { "value": "Escribir en la candidata", "language": "es" } ] }, "party_id": null, "image_uri": null, "is_write_in": true }, { "object_id": "ozark-chief-justice-retain-demergue-affirmative", "name": { "text": [ { "value": "Retain", "language": "en" }, { "value": "Conservar", "language": "es" } ] }, "party_id": null, "image_uri": null, "is_write_in": null }, { "object_id": "ozark-chief-justice-retain-demergue-negative", "name": { "text": [ { "value": "Reject", "language": "en" }, { "value": "Rechazar", "language": "es" } ] }, "party_id": null, "image_uri": null, "is_write_in": null }, { "object_id": "exeter-utility-district-referendum-affirmative", "name": { "text": [ { "value": "Yes", "language": "en" } ] }, "party_id": null, "image_uri": null, "is_write_in": null }, { "object_id": "exeter-utility-district-referendum-negative", "name": { "text": [ { "value": "No", "language": "en" } ] }, "party_id": null, "image_uri": null, "is_write_in": null } ], "contests": [ { "object_id": "president-vice-president-contest", "sequence_order": 0, "electoral_district_id": "hamilton-county", "vote_variation": "one_of_m", "number_elected": 1, "votes_allowed": 1, "name": "President and Vice President of the United States", "ballot_selections": [ { "object_id": "barchi-hallaren-selection", "sequence_order": 0, "candidate_id": "barchi-hallaren" }, { "object_id": "cramer-vuocolo-selection", "sequence_order": 1, "candidate_id": "cramer-vuocolo" }, { "object_id": "court-blumhardt-selection", "sequence_order": 2, "candidate_id": "court-blumhardt" }, { "object_id": "boone-lian-selection", "sequence_order": 3, "candidate_id": "boone-lian" }, { "object_id": "hildebrand-garritty-selection", "sequence_order": 4, "candidate_id": "hildebrand-garritty" }, { "object_id": "patterson-lariviere-selection", "sequence_order": 5, "candidate_id": "patterson-lariviere" }, { "object_id": "write-in-selection-president", "sequence_order": 6, "candidate_id": "write-in" } ], "ballot_title": { "text": [ { "value": "President and Vice President of the United States", "language": "en" }, { "value": "Presidente y Vicepresidente de los Estados Unidos", "language": "es" } ] }, "ballot_subtitle": { "text": [ { "value": "Vote for one", "language": "en" }, { "value": "Votar por uno", "language": "es" } ] } }, { "object_id": "ozark-governor", "sequence_order": 1, "electoral_district_id": "hamilton-county", "vote_variation": "one_of_m", "number_elected": 1, "votes_allowed": 1, "name": "Governor of the Commonwealth of Ozark", "ballot_selections": [ { "object_id": "franz-selection", "sequence_order": 0, "candidate_id": "franz" }, { "object_id": "harris-selection", "sequence_order": 1, "candidate_id": "harris" }, { "object_id": "bargmann-selection", "sequence_order": 2, "candidate_id": "bargmann" }, { "object_id": "abcock-selection", "sequence_order": 3, "candidate_id": "abcock" }, { "object_id": "steel-loy-selection", "sequence_order": 4, "candidate_id": "steel-loy" }, { "object_id": "sharp-selection", "sequence_order": 5, "candidate_id": "sharp" }, { "object_id": "walace-selection", "sequence_order": 6, "candidate_id": "wallace" }, { "object_id": "williams-selection", "sequence_order": 7, "candidate_id": "williams" }, { "object_id": "alpern-selection", "sequence_order": 9, "candidate_id": "alpern" }, { "object_id": "windbeck-selection", "sequence_order": 10, "candidate_id": "windbeck" }, { "object_id": "sharp-althea-selection", "sequence_order": 11, "candidate_id": "sharp-althea" }, { "object_id": "greher-selection", "sequence_order": 12, "candidate_id": "greher" }, { "object_id": "alexander-selection", "sequence_order": 13, "candidate_id": "alexander" }, { "object_id": "mitchell-selection", "sequence_order": 14, "candidate_id": "mitchell" }, { "object_id": "lee-selection", "sequence_order": 15, "candidate_id": "lee" }, { "object_id": "ash-selection", "sequence_order": 16, "candidate_id": "ash" }, { "object_id": "kennedy-selection", "sequence_order": 17, "candidate_id": "kennedy" }, { "object_id": "jackson-selection", "sequence_order": 18, "candidate_id": "jackson" }, { "object_id": "brown-selection", "sequence_order": 19, "candidate_id": "brown" }, { "object_id": "teller-selection", "sequence_order": 20, "candidate_id": "teller" }, { "object_id": "ward-selection", "sequence_order": 21, "candidate_id": "ward" }, { "object_id": "murphy-selection", "sequence_order": 22, "candidate_id": "murphy" }, { "object_id": "newman-selection", "sequence_order": 23, "candidate_id": "newman" }, { "object_id": "callanann-selection", "sequence_order": 24, "candidate_id": "callanann" }, { "object_id": "york-selection", "sequence_order": 25, "candidate_id": "york" }, { "object_id": "chandler-selection", "sequence_order": 26, "candidate_id": "chandler" }, { "object_id": "write-in-selection-governor", "sequence_order": 27, "candidate_id": "write-in" } ], "ballot_title": { "text": [ { "value": "Governor of the Commonwealth of Ozark", "language": "en" }, { "value": "Gobernador de la Mancomunidad de Ozark", "language": "es" } ] }, "ballot_subtitle": { "text": [ { "value": "Vote for one", "language": "en" }, { "value": "Votar por uno", "language": "es" } ] } }, { "object_id": "congress-district-5-contest", "sequence_order": 2, "electoral_district_id": "congress-district-5", "vote_variation": "one_of_m", "number_elected": 1, "votes_allowed": 1, "name": "Congressional District 5", "ballot_selections": [ { "object_id": "soliz-selection", "sequence_order": 0, "candidate_id": "soliz" }, { "object_id": "keller-selection", "sequence_order": 1, "candidate_id": "keller" }, { "object_id": "rangel-selection", "sequence_order": 2, "candidate_id": "rengel" }, { "object_id": "argent-selection", "sequence_order": 3, "candidate_id": "argent" }, { "object_id": "witherspoon-smithson-selection", "sequence_order": 4, "candidate_id": "witherspoon-smithson" }, { "object_id": "write-in-selection-us-congress-district-5", "sequence_order": 5, "candidate_id": "write-in" } ], "ballot_title": { "text": [ { "value": "House of Representatives Congressional District 5", "language": "en" }, { "value": "C\u00e1mara de Representantes de Distrito 5 del Congreso de Ozark", "language": "es" } ] }, "ballot_subtitle": { "text": [ { "value": "Vote for one", "language": "en" }, { "value": "Votar por uno", "language": "es" } ] } }, { "object_id": "congress-district-7-contest", "sequence_order": 3, "electoral_district_id": "congress-district-7", "vote_variation": "one_of_m", "number_elected": 1, "votes_allowed": 1, "name": "Congressional District 7", "ballot_selections": [ { "object_id": "bainbridge-selection", "sequence_order": 0, "candidate_id": "bainbridge" }, { "object_id": "hennessey-selection", "sequence_order": 1, "candidate_id": "hennessey" }, { "object_id": "savoy-selection", "sequence_order": 2, "candidate_id": "savoy" }, { "object_id": "tawa-selection", "sequence_order": 3, "candidate_id": "tawa" }, { "object_id": "tawa-mary-selection", "sequence_order": 4, "candidate_id": "tawa-mary" }, { "object_id": "write-in-selection-us-congress-district-7", "sequence_order": 5, "candidate_id": "write-in" } ], "ballot_title": { "text": [ { "value": "House of Representatives Ozark Congressional District 7", "language": "en" }, { "value": "C\u00e1mara de Representantes de Distrito 7 del Congreso de Ozark", "language": "es" } ] }, "ballot_subtitle": { "text": [ { "value": "Vote for one", "language": "en" }, { "value": "Votar por uno", "language": "es" } ] } }, { "object_id": "pismo-beach-school-board-contest", "sequence_order": 4, "electoral_district_id": "pismo-beach-school-district-precinct-1", "vote_variation": "n_of_m", "number_elected": 3, "votes_allowed": 3, "name": "Pismo Beach School Board", "ballot_selections": [ { "object_id": "moore-selection", "sequence_order": 0, "candidate_id": "moore" }, { "object_id": "white-selection", "sequence_order": 1, "candidate_id": "white" }, { "object_id": "smallberries-selection", "sequence_order": 2, "candidate_id": "smallberries" }, { "object_id": "warfin-selection", "sequence_order": 3, "candidate_id": "warfin" }, { "object_id": "norberg-selection", "sequence_order": 4, "candidate_id": "norberg" }, { "object_id": "parks-selection", "sequence_order": 5, "candidate_id": "parks" }, { "object_id": "savannah-selection", "sequence_order": 6, "candidate_id": "savannah" }, { "object_id": "write-in-selection-1-pismo-beach-school-board", "sequence_order": 7, "candidate_id": "write-in-1" }, { "object_id": "write-in-selection-2-pismo-beach-school-board", "sequence_order": 8, "candidate_id": "write-in-2" }, { "object_id": "write-in-selection-3-pismo-beach-school-board", "sequence_order": 9, "candidate_id": "write-in-3" } ], "ballot_title": { "text": [ { "value": "Pismo Beach School Board", "language": "en" }, { "value": "Junta Escolar de Pismo Beach", "language": "es" } ] }, "ballot_subtitle": { "text": [ { "value": "Vote for up to 3", "language": "en" }, { "value": "Vote por hasta 3", "language": "es" } ] } }, { "object_id": "somerset-school-board-contest", "sequence_order": 5, "electoral_district_id": "somerset-school-district-precinct-1", "vote_variation": "n_of_m", "number_elected": 2, "votes_allowed": 2, "name": "Somerset School Board", "ballot_selections": [ { "object_id": "summers-selection", "sequence_order": 0, "candidate_id": "summers" }, { "object_id": "chase-selection", "sequence_order": 1, "candidate_id": "chase" }, { "object_id": "osborne-selection", "sequence_order": 2, "candidate_id": "osborne" }, { "object_id": "rosenberg-selection", "sequence_order": 3, "candidate_id": "rosenberg" }, { "object_id": "head-selection", "sequence_order": 4, "candidate_id": "head" }, { "object_id": "marsters-selection", "sequence_order": 5, "candidate_id": "marsters" }, { "object_id": "write-in-selection-1-somerset-school-board", "sequence_order": 6, "candidate_id": "write-in-1" }, { "object_id": "write-in-selection-2-somerset-school-board", "sequence_order": 7, "candidate_id": "write-in-2" } ], "ballot_title": { "text": [ { "value": "Pismo Beach School Board", "language": "en" }, { "value": "Junta Escolar de Somerset", "language": "es" } ] }, "ballot_subtitle": { "text": [ { "value": "Vote for up to 2", "language": "en" }, { "value": "Vote por hasta 2", "language": "es" } ] } }, { "object_id": "arlington-chief-justice-retain-demergue", "sequence_order": 6, "electoral_district_id": "arlington-township-precinct-1", "vote_variation": "one_of_m", "number_elected": 1, "votes_allowed": 1, "name": "Retain Robert Demergue as Chief Justice?", "ballot_selections": [ { "object_id": "ozark-chief-justice-retain-demergue-affirmative-selection", "sequence_order": 0, "candidate_id": "ozark-chief-justice-retain-demergue-affirmative" }, { "object_id": "ozark-chief-justice-retain-demergue-negative-selection", "sequence_order": 1, "candidate_id": "ozark-chief-justice-retain-demergue-negative" } ], "ballot_title": { "text": [ { "value": "Retain Robert Demergue as Chief Justice?", "language": "en" }, { "value": "\u00bfRetener a Robert Demergue como Presidente del Tribunal Supremo?", "language": "es" } ] }, "ballot_subtitle": { "text": [ { "value": "Choose 'Accept' or 'Reject'", "language": "en" }, { "value": "Elija 'Aceptar' o 'Rechazar'", "language": "es" } ] } }, { "object_id": "exeter-utility-district-referendum-contest", "sequence_order": 7, "electoral_district_id": "lacroix-exeter-utility-district", "vote_variation": "one_of_m", "number_elected": 1, "votes_allowed": 1, "name": "Capital Projects Levy", "ballot_selections": [ { "object_id": "exeter-utility-district-referendum-affirmative-selection", "sequence_order": 0, "candidate_id": "exeter-utility-district-referendum-affirmative" }, { "object_id": "exeter-utility-district-referendum-selection", "sequence_order": 1, "candidate_id": "exeter-utility-district-referendum-negative" } ], "ballot_title": { "text": [ { "value": "Levy Lift to Maintain Public Safety and Other Core Utility Services", "language": "en" }, { "value": "Levy Lift para Mantener la Seguridad P\u00fablica y Otros Servicios B\u00e1sicos", "language": "es" } ] }, "ballot_subtitle": { "text": [ { "value": "Should this Proposition be approved?", "language": "en" }, { "value": "Uno", "language": "es" } ] } } ], "ballot_styles": [ { "object_id": "congress-district-7-hamilton-county", "geopolitical_unit_ids": [ "hamilton-county", "congress-district-7" ], "party_ids": null, "image_uri": null }, { "object_id": "congress-district-7-lacroix", "geopolitical_unit_ids": [ "hamilton-county", "congress-district-7", "lacroix-township-precinct-1" ], "party_ids": null, "image_uri": null }, { "object_id": "congress-district-7-lacroix-exeter", "geopolitical_unit_ids": [ "hamilton-county", "congress-district-7", "lacroix-township-precinct-1", "lacroix-exeter-utility-district" ], "party_ids": null, "image_uri": null }, { "object_id": "congress-district-7-arlington", "geopolitical_unit_ids": [ "hamilton-county", "congress-district-7", "arlington-township-precinct-1" ], "party_ids": null, "image_uri": null }, { "object_id": "congress-district-7-arlington-pismo-beach", "geopolitical_unit_ids": [ "hamilton-county", "congress-district-7", "arlington-township-precinct-1", "pismo-beach-school-district-precinct-1" ], "party_ids": null, "image_uri": null }, { "object_id": "congress-district-7-arlington-somerset", "geopolitical_unit_ids": [ "hamilton-county", "congress-district-7", "arlington-township-precinct-1", "somerset-school-district-precinct-1" ], "party_ids": null, "image_uri": null }, { "object_id": "congress-district-5-hamilton-county", "geopolitical_unit_ids": [ "hamilton-county", "congress-district-5" ], "party_ids": null, "image_uri": null }, { "object_id": "congress-district-5-lacroix", "geopolitical_unit_ids": [ "hamilton-county", "congress-district-5", "lacroix-township-precinct-1" ], "party_ids": null, "image_uri": null }, { "object_id": "congress-district-5-harris", "geopolitical_unit_ids": [ "hamilton-county", "congress-district-5", "harris-township" ], "party_ids": null, "image_uri": null }, { "object_id": "congress-district-5-arlington-pismo-beach", "geopolitical_unit_ids": [ "hamilton-county", "congress-district-5", "arlington-township-precinct-1", "pismo-beach-school-district-precinct-1" ], "party_ids": null, "image_uri": null }, { "object_id": "congress-district-5-arlington-somerset", "geopolitical_unit_ids": [ "hamilton-county", "congress-district-5", "arlington-township-precinct-1", "somerset-school-district-precinct-1" ], "party_ids": null, "image_uri": null } ], "name": { "text": [ { "value": "Hamiltion County General Election", "language": "en" }, { "value": "Elecci\u00f3n general del condado de Hamilton", "language": "es" } ] }, "contact_information": { "address_line": [ "1234 Paul Revere Run", "Hamilton, Ozark 99999" ], "email": [ { "annotation": "press", "value": "inquiries@hamilton.state.gov" }, { "annotation": "federal", "value": "commissioner@hamilton.state.gov" } ], "phone": [ { "annotation": "domestic", "value": "123-456-7890" }, { "annotation": "international", "value": "+1-123-456-7890" } ], "name": "Hamilton State Election Commission" } } ================================================ FILE: data/manifest-minimal.json ================================================ { "election_scope_id": "franklin-minimal-referendum-manifest", "spec_version": "1.0", "type": "general", "start_date": "2020-03-01T08:00:00-05:00", "end_date": "2020-03-03T19:00:00-05:00", "geopolitical_units": [ { "object_id": "franklin-county", "name": "Franklin County", "type": "municipality", "contact_information": null } ], "parties": [ { "object_id": "N/A", "name": { "text": [] }, "abbreviation": null, "color": null, "logo_uri": null } ], "candidates": [ { "object_id": "referendum-pineapple-affirmative", "name": { "text": [] }, "party_id": null, "image_uri": null, "is_write_in": null }, { "object_id": "referendum-pineapple-negative", "name": { "text": [] }, "party_id": null, "image_uri": null, "is_write_in": null } ], "contests": [ { "object_id": "referendum-pineapple", "sequence_order": 0, "electoral_district_id": "franklin-county", "vote_variation": "one_of_m", "number_elected": 1, "votes_allowed": 1, "name": "Referendum for Banning Pineapple on Pizza", "ballot_selections": [ { "object_id": "referendum-pineapple-affirmative-selection", "sequence_order": 0, "candidate_id": "referendum-pineapple-affirmative" }, { "object_id": "referendum-pineapple-negative-selection", "sequence_order": 1, "candidate_id": "referendum-pineapple-negative" } ], "ballot_title": null, "ballot_subtitle": null } ], "ballot_styles": [ { "object_id": "ballot-style-01", "geopolitical_unit_ids": [ "franklin-county" ], "party_ids": null, "image_uri": null } ], "name": { "text": [ { "value": "Franklin County Minimal General Election March 2020", "language": "en" }, { "value": "Elecciones generales m\u00ednimas del condado de Franklin marzo de 2020", "language": "es" } ] }, "contact_information": null } ================================================ FILE: data/manifest-small.json ================================================ { "election_scope_id": "franklin-county-general-march2020", "spec_version": "1.0", "type": "general", "start_date": "2020-03-01T08:00:00-05:00", "end_date": "2020-03-03T19:00:00-05:00", "geopolitical_units": [ { "object_id": "franklin-county", "name": "Franklin County", "type": "county", "contact_information": null }, { "object_id": "ozark-school-district", "name": "Ozark School District in Franklin County", "type": "school", "contact_information": null } ], "parties": [ { "object_id": "party-whig", "name": { "text": [] }, "abbreviation": "WHI", "color": null, "logo_uri": null }, { "object_id": "party-federalist", "name": { "text": [] }, "abbreviation": "FED", "color": null, "logo_uri": null }, { "object_id": "party-democratic-republican", "name": { "text": [] }, "abbreviation": "DR", "color": null, "logo_uri": null } ], "candidates": [ { "object_id": "benjamin-franklin", "name": { "text": [] }, "party_id": "party-whig", "image_uri": null, "is_write_in": null }, { "object_id": "john-adams", "name": { "text": [] }, "party_id": "party-federalist", "image_uri": null, "is_write_in": null }, { "object_id": "john-hancock", "name": { "text": [] }, "party_id": "party-democratic-republican", "image_uri": null, "is_write_in": null }, { "object_id": "samuel-adams", "name": { "text": [] }, "party_id": "party-democratic-republican", "image_uri": null, "is_write_in": null }, { "object_id": "thomas-jefferson", "name": { "text": [] }, "party_id": "party-democratic-republican", "image_uri": null, "is_write_in": null }, { "object_id": "write-in", "name": { "text": [] }, "party_id": null, "image_uri": null, "is_write_in": true }, { "object_id": "referendum-pineapple-affirmative", "name": { "text": [] }, "party_id": null, "image_uri": null, "is_write_in": null }, { "object_id": "referendum-pineapple-negative", "name": { "text": [] }, "party_id": null, "image_uri": null, "is_write_in": null } ], "contests": [ { "object_id": "congressional-race-01", "sequence_order": 0, "electoral_district_id": "franklin-county", "vote_variation": "one_of_m", "number_elected": 1, "votes_allowed": 1, "name": "Representative to US Congress", "ballot_selections": [ { "object_id": "john-hancock-selection", "sequence_order": 0, "candidate_id": "john-hancock" }, { "object_id": "benjamin-franklin-selection", "sequence_order": 1, "candidate_id": "benjamin-franklin" }, { "object_id": "write-in-selection", "sequence_order": 2, "candidate_id": "write-in" } ], "ballot_title": null, "ballot_subtitle": null }, { "object_id": "referendum-pineapple", "sequence_order": 1, "electoral_district_id": "franklin-county", "vote_variation": "one_of_m", "number_elected": 1, "votes_allowed": 1, "name": "The Pineapple Question", "ballot_selections": [ { "object_id": "referendum-pineapple-affirmative-selection", "sequence_order": 0, "candidate_id": "referendum-pineapple-affirmative" }, { "object_id": "referendum-pineapple-negative-selection", "sequence_order": 1, "candidate_id": "referendum-pineapple-negative" } ], "ballot_title": null, "ballot_subtitle": null }, { "object_id": "ozark-school-board-race", "sequence_order": 2, "electoral_district_id": "ozark-school-district", "vote_variation": "n_of_m", "number_elected": 2, "votes_allowed": 2, "name": "Ozark School Board", "ballot_selections": [ { "object_id": "john-adams-selection", "sequence_order": 0, "candidate_id": "john-adams" }, { "object_id": "samuel-adams-selection", "sequence_order": 1, "candidate_id": "samuel-adams" }, { "object_id": "thomas-jefferson-selection", "sequence_order": 2, "candidate_id": "thomas-jefferson" } ], "ballot_title": null, "ballot_subtitle": null } ], "ballot_styles": [ { "object_id": "franklin-county-no-schooldistrict", "geopolitical_unit_ids": [ "franklin-county" ], "party_ids": null, "image_uri": null }, { "object_id": "franklin-county-ozark-schools", "geopolitical_unit_ids": [ "franklin-county", "ozark-school-district" ], "party_ids": null, "image_uri": null } ], "name": { "text": [ { "value": "Franklin County Small General Election March 2020", "language": "en" }, { "value": "Peque\u00f1as elecciones generales del condado de Franklin marzo de 2020", "language": "es" } ] }, "contact_information": null } ================================================ FILE: data/plaintext_ballots_simple.json ================================================ [ { "object_id": "1048ce32-f1b1-4b05-b7fb-8c615ac842ee", "style_id": "jefferson-county-ballot-style", "contests": [ { "object_id": "justice-supreme-court", "ballot_selections": [ { "object_id": "john-adams-selection", "vote": 1 }, { "object_id": "write-in-selection", "vote": 1, "extended_data": { "value": "Susan B. Anthony", "length": 16 } } ] } ] }, { "object_id": "03a29d15-667c-4ac8-afd7-549f19b8e4eb", "style_id": "jefferson-county-ballot-style", "contests": [ { "object_id": "justice-supreme-court", "ballot_selections": [ { "object_id": "john-adams-selection", "vote": 1 }, { "object_id": "write-in-selection", "vote": 1, "extended_data": { "value": "Susan B. Anthony", "length": 16 } } ] } ] }, { "object_id": "25a7111b-4334-425a-87c1-f7a49f42b3a2", "style_id": "jefferson-county-ballot-style", "contests": [ { "object_id": "justice-supreme-court", "ballot_selections": [ { "object_id": "john-adams-selection", "vote": 1 }, { "object_id": "benjamin-franklin-selection", "vote": 1 } ] } ] }, { "object_id": "69aeacb4-64c6-4205-9bb2-5fb6b3b3ea58", "style_id": "harrison-township-ballot-style", "contests": [ { "object_id": "justice-supreme-court", "ballot_selections": [ { "object_id": "john-adams-selection", "vote": 1 }, { "object_id": "john-hancock-selection", "vote": 1 } ] } ] }, { "object_id": "5a150c74-a2cb-47f6-b575-165ba8a4ce53", "style_id": "harrison-township-ballot-style", "contests": [ { "object_id": "justice-supreme-court", "ballot_selections": [ { "object_id": "john-adams-selection", "vote": 1 }, { "object_id": "john-hancock-selection", "vote": 1 } ] }, { "object_id": "referendum-pineapple", "ballot_selections": [ { "object_id": "referendum-pineapple-affirmative-selection", "vote": 1 } ] } ] }, { "object_id": "9fee0e77-cfd2-401a-a210-93bbc4dd30ef", "style_id": "harrison-township-ballot-style", "contests": [ { "object_id": "justice-supreme-court", "ballot_selections": [ { "object_id": "john-adams-selection", "vote": 1 }, { "object_id": "write-in-selection", "vote": 1, "extended_data": { "value": "Susan B. Anthony", "length": 16 } } ] }, { "object_id": "referendum-pineapple", "ballot_selections": [ { "object_id": "referendum-pineapple-negative-selection", "vote": 1 } ] } ] } ] ================================================ FILE: data/plaintext_two_ballots_minimal.json ================================================ [ { "object_id": "external-ballot-id-1234", "style_id": "ballot-style-01", "contests": [ { "object_id": "referendum-pineapple", "ballot_selections": [ { "object_id": "referendum-pineapple-affirmative-selection", "vote": 1 } ] } ] }, { "object_id": "external-ballot-id-3457", "style_id": "ballot-style-01", "contests": [ { "object_id": "referendum-pineapple", "ballot_selections": [ { "object_id": "referendum-pineapple-negative-selection", "vote": 1 } ] } ] } ] ================================================ FILE: data/plaintext_two_ballots_small.json ================================================ [ { "object_id": "external-ballot-id-2345", "style_id": "franklin-county-ozark-schools", "contests": [ { "object_id": "referendum-pineapple", "ballot_selections": [ { "object_id": "referendum-pineapple-affirmative-selection", "vote": 1 } ] }, { "object_id": "congressional-race-01", "ballot_selections": [ { "object_id": "john-hancock-selection", "vote": 1 } ] }, { "object_id": "ozark-school-board-race", "ballot_selections": [ { "object_id": "samuel-adams-selection", "vote": 1 }, { "object_id": "thomas-jefferson-selection", "vote": 1 } ] } ] }, { "object_id": "external-ballot-id-9876", "style_id": "franklin-county-no-schooldistrict", "contests": [ { "object_id": "referendum-pineapple", "ballot_selections": [ { "object_id": "referendum-pineapple-affirmative-selection", "vote": 1 } ] }, { "object_id": "congressional-race-01", "ballot_selections": [ { "object_id": "benjamin-franklin-selection", "vote": 1 } ] } ] } ] ================================================ FILE: docs/0_Configure_Election.ipynb ================================================ { "cells": [ { "cell_type": "markdown", "source": [ "# Election Configuration\n", "\n", "An election in ElectionGuard is defined as a set of metadata and cryptographic artifacts necessary to encrypt, conduct, tally, decrypt, and verify an election. The Data format used for election metadata is based on the [NIST Election Common Standard Data Specification](https://www.nist.gov/itl/voting/interoperability) but includes some modifications to support the end-to-end cryptography of ElectionGuard.\n", "\n", "Election metadata is described in a specific format parseable into an `Manifest` and it's validity is checked to ensure that it is of an appropriate structure to conduct an End-to-End Verified ElectionGuard Election. ElectionGuard only verifies the components of the election metadata that are necessary to encrypt and decrypt the election. Some components of the election metadata are not checked for structural validity, but are used when generating a hash representation of the `Manifest`.\n", "\n", "From an `Manifest` we derive an `InternalManifest` that includes a subset of the elements from the `Manifest` required to verify ballots are correct. Additionally a `CiphertextElectionContext` is created during the [Key Ceremony](/1_Key_Ceremony.md) that includes the cryptographic artifacts necessary for encrypting ballots.\n", "\n", "## Glossary\n", "\n", "- **Election Manifest** The election metadata in json format that is parsed into an Election Description\n", "- **Election Description** The election metadata that describes the structure and type of the election, including geopolitical units, contests, candidates, and ballot styles, etc.\n", "- **Internal Election Description** The subset of the `Manifest` required by ElectionGuard to validate ballots are correctly associated with an election. This component mutates the state of the Election Description.\n", "- **Ciphertext Election Context** The cryptographic context of an election that is configured during the `Key Ceremony`\n", "- **Description Hash** a Hash representation of the original Manifest.\n", "\n", "## Process\n", "\n", "1. Define an election according to the `Manifest` requirements.\n", "2. Use the [NIST Common Standard Data Specification](https://www.nist.gov/itl/voting/interoperability) as a guide, but note the differences in [election.py](https://github.com/microsoft/electionguard-python/tree/main/src/electionguard.election.py) and the provided [sample manifest](https://github.com/microsoft/electionguard-python/tree/main/data/election_manifest_simple.json).\n", "3. Parse the `Manifest` into the application.\n", "4. Define the encryption parameters necessary for conducting an election (see `Key Ceremony`).\n", "5. Create the Pubic Key either from a single secret, or from the Key Ceremony.\n", "6. Build the `InternalManifest` and `CiphertextElectionContext` from the `Manifest` and `ElGamalKeyPair.public_key`.\n", "\n", "## Usage Example" ], "metadata": {} }, { "cell_type": "code", "execution_count": null, "source": [ "import os\n", "from electionguard.election import CiphertextElectionContext\n", "from electionguard.election_builder import ElectionBuilder\n", "from electionguard.elgamal import ElGamalKeyPair, elgamal_keypair_from_secret\n", "from electionguard.manifest import Manifest, InternalManifest\n", "\n", "# Open an election manifest file\n", "with open(os.path.join(some_path, \"election-manifest.json\"), \"r\") as manifest:\n", " string_representation = manifest.read()\n", " election_description = Manifest.from_json(string_representation)\n", "\n", "# Create an election builder instance, and configure it for a single public-private keypair.\n", "# in a real election, you would configure this for a group of guardians. See Key Ceremony for more information.\n", "builder = ElectionBuilder(\n", " number_of_guardians=1, # since we will generate a single public-private keypair, we set this to 1\n", " quorum=1, # since we will generate a single public-private keypair, we set this to 1\n", " description=election_description,\n", ")\n", "\n", "# Generate an ElGamal Keypair from a secret. In a real election you would use the Key Ceremony instead.\n", "some_secret_value: int = 12345\n", "keypair: ElGamalKeyPair = elgamal_keypair_from_secret(some_secret_value)\n", "\n", "builder.set_public_key(keypair.public_key)\n", "\n", "# get an `InternalElectionDescription` and `CiphertextElectionContext`\n", "# that are used for the remainder of the election.\n", "(internal_manifest, context) = builder.build()" ], "outputs": [], "metadata": { "attributes": { "classes": [ "code-cell" ], "id": "" } } }, { "cell_type": "markdown", "source": [ "## Constants\n", "\n", "The election constants are the four constants that sit underneath most of the mathematical operations. The election constants can be configured, but there is a standard set which is recommended for most use cases. These can be found in `constants.py`.\n", "\n", "**⚠️ Warning ⚠️**\n", "\n", "There are some test constants used for testing code, but these are never to be used in any production system. The small and extra small constants are unlikely to be used except in rare cases for property testing due to collisions that will happen for smaller number sets." ], "metadata": {} } ], "metadata": { "language_info": { "name": "python" } }, "nbformat": 4, "nbformat_minor": 5 } ================================================ FILE: docs/1_Key_Ceremony.md ================================================ # Key Ceremony The ElectionGuard Key Ceremony is the process used by Election Officials to share encryption keys for an election. Before an election, a fixed number of Guardians are selection to hold the private keys needed to decrypt the election results. A Quorum count of Guardians can also be specified to compensate for guardians who may be missing at the time of Decryption. For instance, 5 Guardians may be selected to hold the keys, but only 3 of them are required to decrypt the election results. Guardians are typically Election Officials, Trustees Canvass Board Members, Government Officials or other trusted authorities who are responsible and accountable for conducting the election. ## Summary The Key Ceremony is broken into several high-level steps. Each Guardian must _announce_ their _attendance_ in the key ceremony, generate their own public-private key pairs, and then _share_ those key pairs with the Quorum. Then the data that is shared is mathematically verified using Non-Interactive Zero Knowledge Proofs, and finally a _joint public key_ is created to encrypt ballots in the election. ### Attendance Guardians exchange all public keys and ensure each fellow guardian has received an election public key ensuring at all guardians are in attendance. ### Key Sharing Guardians generate a partial key backup for each guardian and share with that designated key with that guardian. Then each designated guardian sends a verification back to the sender. The sender then publishes to the group when all verifications are received. ### Joint Key The final step is to publish the joint election key after all keys and backups have been shared. ## Glossary - **Guardian** A guardian of the election who holds the ability to partially decrypt the election results - **Key Ceremony Mediator** A mediator to mediate communication (if needed) of information such as keys between the guardians - **Election Key Pair:** Pair of keys (public & secret) used to encrypt/decrypt election - **Election Partial Key Backup:** A point on a secret polynomial and commitments to verify this point for a designated guardian. - **Election Polynomial:** The election polynomial is the mathematical expression that each Guardian defines to solve for his or her private key. A different point associated with the polynomial is shared with each of the other guardians so that the guardians can come together to derive the polynomial function and solve for the private key. - **Joint Key:** Combined public key from election public keys of each guardian - **Quorum:** Quantity of guardians (k) that is required to decrypt the election and is fewer than the total number of guardians available (n) ## Process This is a detailed description of the entire Key Ceremony Process 1. The ceremony details are decided upon. These include a `number_of_guardians` and `quorum` of guardians required for decryption. 2. Each guardian creates a unique `id` and `sequence_order`. 3. Each guardian must generate their `election key pair` _(ElGamal key pair)_. This will generate a corresponding Schnorr `proof` and `polynomial` used for generating `election partial key backups` for sharing. 4. Each guardian must give the other guardians their `election public key` directly or through a mediator. 5. Each guardian must check if all `election public keys` are received. 6. Each guardian must generate `election partial key backup` for each other guardian. The guardian will use their `polynomial` and the designated guardian's `sequence_order` to create the value. 7. Each guardian must send each encrypted `election partial key backup` to the designated guardian directly or through a `mediator`. 8. Each guardian checks if all encrypted `election partial key backups` have been received by their recipient guardian directly or through a mediator. 9. Each recipient guardian decrypts each received encrypted `election partial key backup` 10. Each recipient guardian verifies each `election partial key backup` and sends confirmation of verification - If the proof verifies, continue - If the proof fails 1. Sender guardian publishes the `election partial key backup` value sent to recipient as a `election partial key challenge` to all the other guardians 2. Alternate guardian (outside sender or original recipient) attempts to verify key - If the proof verifies, continue - If the proof fails again, the accused (sender guardian) should be evicted and process should be restarted with new guardian. 11. On receipt of all verifications of `election partial private keys` by all guardians, generate and publish `joint key` from election public keys. ## Files - [`key_ceremony.py`](https://github.com/microsoft/electionguard-python/tree/main/src/electionguard/key_ceremony.py) - [`guardian.py`](https://github.com/microsoft/electionguard-python/tree/main/src/electionguard/guardian.py) - [`key_ceremony_mediator.py`](https://github.com/microsoft/electionguard-python/tree/main/src/electionguard/key_ceremony_mediator.py) ## Usage Example This example demonstrates a convenience method to generate guardians for an election ```python NUMBER_OF_GUARDIANS: int QUORUM: int details: CeremonyDetails guardians: List[Guardian] # Setup Guardians for i in range(NUMBER_OF_GUARDIANS): guardians.append( Guardian.from_nonce(f"some_guardian_id_{str(i)}", i, NUMBER_OF_GUARDIANS, QUORUM) ) mediator = KeyCeremonyMediator(details) # Attendance (Public Key Share) for guardian in guardians: mediator.announce(guardian) # Orchestation (Private Key Share) orchestrated = mediator.orchestrate() # Verify (Prove the guardians acted in good faith) verified = mediator.verify() # Publish the Joint Public Key joint_public_key = mediator.publish_joint_key() ``` ## Implementation Considerations ElectionGuard can be run without the key ceremony. The key ceremony is the recommended process to generate keys for live end-to-end verifiable elections, however this process may not be necessary for other use cases such as privacy preserving risk limiting audits. ================================================ FILE: docs/2_Encrypt_Ballots.md ================================================ # Encrypt Ballots The primary function of ElectionGuard is to encrypt ballots. Ballots are encrypted on a uniquely identified device within the context of a specific election. The election public key is used to encrypt ballots. A _master nonce_ value is generated for each ballot and the nonce is used to derive other nonce values for encrypting the selection on each ballot. ## Glossary - **Plaintext Ballot** - The plaintext representation of a voter's selections - **Ciphertext Ballot** - The encrypted representation of a voter's selections - **Master Nonce** - A random number used to derive encryptions in a `CiphertextBallot` - **Verification Code or Ballot Code** - A unique hash value generated by an _Encryption Device_ to anonymously identify a ballot - **Encryption Device** The device that is doing the encryption ## Process 1. Verify the ballot is well-formed against the _Election Metadata_ (`InternalManifest`) 2. Generate a random master nonce value to use as a secret when encrypting the ballot 3. Using the metadata of the election and the master nonce, encrypt each selection on the ballot 4. For each selection on the ballot, generate a disjunctive Non-Interactive Zero-Knowledge Proof that the encryption is either an encryption of zero or one 5. For each contest on the ballot, generate a Non-Interactive Zero-Knowledge Proof that the sum of all encrypted ballots is equal to the selection limit on the contest 6. Generate a verification code for the ballot ## Usage Example ```python internal_manifest: InternalManifest context: CiphertextElectionContext ballot: PlaintextBallot # Configure an encryption device device = EncryptionDevice(generate_device_uuid(), "Session", 12345, "polling-place-one") encrypter = EncryptionMediator(internal_manifest, context, device) # Encrypt the ballot encrypted_ballot: CiphertextBallot = encrypter.encrypt(ballot) ``` ## Implementation Considerations When encrypting a ballot, a new ballot object is created that is associated with the plaintext ballot. The encrypted representation includes all of the encryptions, hash values, nonce values, and proofs generated at each step. For the primary end-to-end election workflow, consumers of this API should separate the `nonce` values from the `CiphertextBallot` prior to publishing the encrypted ballot representation. ================================================ FILE: docs/3_Cast_and_Spoil.md ================================================ # Cast and Spoil Ballots Each ballot that is completed by a voter must be either cast or spoiled. A cast ballot is a ballot that the voter accepts as valid and wishes to include in the official election tally. A spoiled ballot, also referred to as a challenged ballot, is a ballot that the voter does not accept as valid and wishes to exclude from the official election tally. ElectionGuard includes a mechanism to mark a specific ballot as either cast or spoiled. Cast ballots are included in the tally record, while spoiled ballots are not. Spoiled ballots are decrypted into plaintext and published along with the rest of the election record. ## Jurisdictional Differences Depending on the jurisdiction conducting an election the process of casting and spoiling ballots may be handled differently. For this reason, there are multiple ways to interact with the `BallotBox` and `Tally`. - By calling [accept_ballot](###-Function-Example) - Ballots can be marked cast or spoiled manually. - By using the [Ballot Box](###-Class-Example) - Ballots can be marked cast or spoiled using a stateful class. ### Unknown Ballots In some jurisdictions, there is a limit on the number of ballots that may be marked as spoiled. If this is the case, you may use the `BallotBoxState.UNKNOWN` state, or extend the enumeration to support your specific use case. ## Encrypted Tally Once all of the ballots are marked as _cast_ or _spoiled_, all of the encryptions of each option are homomorphically combined to form an encryption of the total number of times that each option was selected in the election. > This process is completed only for cast ballot. > The spoiled ballots are simply marked for inclusion in the election results. ## Glossary - **Ciphertext Ballot** An encrypted representation of a voter's filled-in ballot. - **Submitted Ballot** A wrapper around the `CiphertextBallot` that represents a ballot that is submitted for inclusion in election results and is either: cast or spoiled. - **Ballot Box** A stateful collection of ballots that are either cast or spoiled. - **Ballot Store** A repository for retaining cast and spoiled ballots. - **Cast Ballot** A ballot which a voter has accepted as valid to be included in the official election tally. - **Spoiled Ballot** A ballot which a voter did not accept as valid and is not included in the tally. - **Unknown Ballot** A ballot which may not yet be determined as cast or spoiled, or that may have been spoiled but is otherwise not published in the election results. - **Homomorphic Tally** An encrypted representation of every selection on every ballot that was cast. This representation is stored in a `CiphertextTally` object. ## Process 1. Each ballot is loaded into memory (if it is not already). 2. Each ballot is verified to be correct according to the specific election metadata and encryption context. 3. Each ballot is `submitted` and identified as either being `cast` or `spoiled`. 4. The collection of cast and spoiled ballots is cached in the `DataStore`. 5. All ballots are tallied. The `cast` ballots are combined to create a `CiphertextTally` The spoiled ballots are cached for decryption later. ## Ballot Box The ballot box can be interacted with via a stateful class that caches the election context, or via stateless functions. The following examples demonstrate some ways to interact with the ballot box. Depending on the specific election workflow, the `BallotBox`class may not be used for a given election. For instance, in one case a ballot can be **submitted** directly on an electronic device, in which case there is no `BallotBox`. In a different workflow, a ballot may be explicitly cast or spoiled in a later step, such as after printing for voter review. In all cases, a ballot must be marked as either `cast` or `spoiled` to be included in a tally result. ### Class Example ```python from electionguard.ballot_box import BallotBox internal_manifest: InternalManifest encryption: CiphertextElection store: DataStore ballots_to_cast: List[CiphertextBallot] ballots_to_spoil: List[CiphertextBallot] # The Ballot Box is a thin wrapper around the `accept_ballot` function method ballot_box = BallotBox(internal_manifest, encryption, store) # Cast the ballots for ballot in ballots_to_cast: submitted_ballot = ballot_box.cast(ballot) # The ballot is both returned, and placed into the ballot store assert(store.get(submitted_ballot.object_id) == submitted_ballot) # Spoil the ballots for ballot in ballots_to_spoil: assert(ballot_box.spoil(ballot) is not None) ``` ### Function Example ``` python from electionguard.ballot_box import accept_ballot internal_manifest: InternalManifest encryption: CiphertextElection store: DataStore ballots_to_cast: List[CiphertextBallot] ballots_to_spoil: List[CiphertextBallot] for ballot in ballots_to_cast: submitted_ballot = accept_ballot( ballot, BallotBoxState.CAST, internal_manifest, encryption, store ) for ballot in ballots_to_spoil: submitted_ballot = accept_ballot( ballot, BallotBoxState.SPOILED, internal_manifest, encryption, store ) ``` ## Tally Generating the encrypted `CiphertextTally` can be completed by creating a `CiphertextTally` stateful class and manually marshalling each cast and spoiled ballot. Using this method is preferable when the collection of ballots is very large For convenience, stateless functions are also provided to automatically generate the `CiphertextTally` from a `DataStore`. This method is preferred when the collection of ballots is arbitrarily small, or when the `DataStore` is overloaded with a custom implementation. ### Using the Stateful Class ```python internal_manifest: InternalManifest context: CiphertextElectionContext ballots: List[SubmittedBallot] tally = CiphertextTally(internal_manifest, context) for ballot in ballots: assert(tally.append(ballot)) ``` ### Functional Method ```python internal_manifest: InternalManifest context: CiphertextElectionContext store: DataStore tally = tally_ballots(store, internal_manifest, context) assert(tally is not None) ``` ================================================ FILE: docs/4_Decrypt_Tally.md ================================================ # Decryption At the conclusion of voting, all of the cast ballots are published in their encrypted form in the election record together with the proofs that the ballots are well-formed. Additionally, all of the encryptions of each option are homomorphically-combined to form an encryption of the total number of times that each option was selected. The homomorphically-combined encryptions are decrypted to generate the election tally. Individual cast ballots are not decrypted. Individual spoiled ballots are decrypted and the plaintext values are published along with the encrypted representations and the proofs. In order to decrypt the homomorphically-combined encryption of each selection, each `Guardian` participating in the decryption must compute a specific `Decryption Share` of the decryption. It is preferable that all guardians be present for decryption, however in the event that guardians cannot be present, Electionguard includes a mechanism to decrypt with the `Quorum of Guardians`. During the [Key Ceremony](1_Key_Ceremony.md) a `Quorum of Guardians` is defined that represents the minimum number of guardians that must be present to decrypt the election. If the decryption is to proceed with a `Quorum of Guardians` greater than or equal to the `Quorum` count, but fewer than the total number of guardians, then a subset of the `Available Guardians` must also each construct a `Partial Decryption Share` for the missing `Missing Guardian`, in addition to providing their own `Decryption Share`. It is important to note that mathematically not every present guardian has to compute a `Partial Decryption Share` for every `Missing Guardian`. Only the `Quorum Count` of guardians are necessary to construct `Partial Decryption Shares` in order to compensate for any Missing Guardian. In this implementation, we take an approach that utilizes all Available Guardians to compensate for Missing Guardians. When it is determined that guardians are missing, all available guardians each calculate a `Partial Decryption Share` for the missing guardian and publish the result. A `Quorum of Guardians` count of available `Partial Decryption Shares` is randomly selected from the pool of available partial decryption shares for a given` Missing Guardian`. If more than one guardian is missing, we randomly choose to ignore the `Partial Decryption Share` provided by one of the Available Guardians whose partial decryption share was chosen for the previous Missing Guardian, and randomly select again from the pool of available Partial Decryption Shares. This ensures that all available guardians have the opportunity to participate in compensating for Missing Guardians. ## Glossary - **Guardian** A guardian of the election who holds the ability to partially decrypt the election results - **Decryption Share** A guardian's partial share of a decryption - **Encrypted Tally** The homomorphically-combined and encrypted representation of all selections made for each option on every contest in the election. See [Ballot Box]() for more information. - **Key Ceremony** The process conducted at the beginning of the election to create the joint encryption context for encrypting ballots during the election. See [Key Ceremony](1_Key_Ceremony.md) for more information. - **Quorum of Guardians** The minimum count (_threshold_) of guardians that must be present in order to successfully decrypt the election results. - **Available Guardian** A guardian that has announced as _present_ for the decryption phase - **Missing Guardian** A guardian who was configured during the `Key Ceremony` but who is not present for the decryption of the election results. - **Compensated Decryption Share** - a partial decryption share value computed by an available guardian to compensate for a missing guardian so that the missing guardian's share can be generated and the election results can be successfully decrypted. - **Decryption Mediator** - A component or actor responsible for composing each guardian's partial decryptions or compensated decryptions into the plaintext tally ## Process 1. Each `Guardian` that will participate in the decryption process computes a `Decryption Share` of the _Ciphertext Tally_. 2. Each `Guardian` also computes a Chaum-Pedersen proof of correctness of their `Decryption Share`. ### Decryption when All Guardians are Present 3. If all guardians are present, the Decryption Shares are combined to generate a tally for each option on every contest ### Decryption when some Guardians are Missing _warning: The functionality described in this segment is still a 🚧 Work In Progress_ When one or more of the Guardians are missing, any subset of the Guardians that are present can use the information they have about the other guardian's private keys to reconstruct the partial decryption shares for the missing guardians. 4. Each `Available Guardian` computes a `Partial Decryption Share` for each `Missing Guardian` 5. at least a `Quorum` count of `Partial Decryption Shares` are chosen from the values generated in the previous step for a specific `Missing guardian` 6. Each chosen `Available Guardian` uses its `Partial Decryption Share` to compute a share of the missing partial decryption. 7. the process is re-run until all Missing Guardians are compensated for. 8. The `Compensated Decryption Shares` are combined to _reconstruct_ the missing `TallyDecryptionShare` 9. finally, all of the `DecryptionShares` are combined to generate a tally for each option on every contest ## Challenged/Spoiled Ballots If a ballot is not to be included in the vote count, it is considered challenged, or [Spoiled](https://en.wikipedia.org/wiki/Spoilt_vote). Every ballot spoiled in an election is individually verifiably decrypted in exactly the same way that the aggregate ballot of tallies is decrypted. Since spoiled ballots are not included as part of the vote count, they are included in the Election Record with their plaintext values included along with the encrypted representations. Spoiling ballots is an important part of the ElectionGuard process as it allows voters to explicitly generate challenge ballots that are verifiable as part of the Election Record. ## Usage Example Here is a simple example of how to execute the decryption process. ```python internal_manifest: InternalManifest # Load the election manifest context: CiphertextElectionContext # Load the election encryption context encrypted_Tally: CiphertextTally # Provide a tally from the previous step available_guardians: List[Guardian] # Provite the list of guardians who will participate missing_guardians: List[str] # Provide a list of guardians who will not participate mediator = DecryptionMediator(internal_manifest, context, encrypted_tally) # Loop through the available guardians and annouce their presence for guardian in available_guardians: if (mediator.announce(guardian) is None): break # loop through the missing guardians and compensate for them for guardian in missing_guardians: if (mediator.compensate(guardian) is None): break # Generate the plaintext tally plaintext_tally = mediator.get_plaintext_tally() # The plaintext tally automatically includes the election tally and the spoiled ballots contest_tallies = plaintext_tally.contests spoiled_ballots = plaintext_tally.spoiled_ballots ``` ## Implementation Considerations In certain use cases where the `Key Ceremony` is not used, ballots and tallies can be decrypted directly using the secret key of the election. See the [Tally Tests](https://github.com/microsoft/electionguard-python/tree/main/tests/test_tally.md) for an example of how to decrypt the tally using the secret key. ================================================ FILE: docs/5_Publish_and_Verify.md ================================================ # Publish and Verify ## Publish Publishing the election artifacts helps ensure third parties can verify the election. Refer to the specification on the specific details. Below is a breakdown of the objects within the repository. These are files that should be published at the close of the election so others can verify the election. **Election Artifacts** ```py manifest: Manifest # Manifest constants: ElectionConstants # Constants context: CiphertextElectionContext # Encryption context devices: List[EncryptionDevice] # Encryption devices guardian_records: List[GuardianRecord] # Record of public guardian information submitted_ballots: List[SubmittedBallot] # Encrypted submitted ballots challenge_ballots: List[PlaintextTally] # Decrypted challenge ballots ciphertext_tally: CiphertextTally # Encrypted tally plaintext_tally: PlaintextTally # Decrypted tally ``` These classes have been defined as `dataclass` to ensure that `asdict` can be used. This ensures ease of serialization to dictionaries within python, but allows customization for those wishing to use custom serialization. `electionguard_tools` includes `export.py` which can be used as an example. ## Verify The election artifacts provide a means to begin validation. Start with deserializing the election artifacts to their original classes. ================================================ FILE: docs/Build_and_Run.md ================================================ # Build and Run These instructions can be used to build and run the project. ## Setup ### 1. Initialize dev environment ``` make environment ``` OR ``` poetry install --dev ``` ### 2. Install the `electionguard` module in edit mode ``` make install ``` OR ``` poetry run python -m pip install -e . ``` !!! warning "Note: gmpy2 Windows Installation" **Recommended: Use Windows Subsystem for Linux (WSL)** _WSL supports the generic workflow for installtion._ 1. Install [WSL](https://docs.microsoft.com/en-us/windows/wsl/install). 2. Return to **1. Initialize dev environment** **Alternative: Install pre-compiled binary** _Poetry does not support `pip install --find-links`, so the `pyproject.toml` must be edited and utilize a local pre-compiled binary of the gmpy2 package._ 1. Determine if 64-bit: _The 32 vs 64 bit is based on your installed python version NOT your system._ This code snippet will read true for 64 bit. ```py python -c 'from sys import maxsize; print(maxsize > 2**32)' ``` 2. Download [pre-compiled binary](https://www.lfd.uci.edu/~gohlke/pythonlibs/#gmpy) into project folder 3. Within `pyproject.toml`, replace `gmpy2` reference with direct path to downloaded file. ```py gmpy2 = { path = "./packages/gmpy2-2.0.8-cp39-cp39-win_amd64.whl" } ``` 3. Run `make install` ### 3. Validate import of module _(Optional)_ ``` make validate ``` OR ``` poetry run python -c 'import electionguard; print(electionguard.__package__ + " successfully imported")' ``` ## Running ### Option 1: Code Coverage ``` make coverage ``` OR ``` poetry run coverage report ``` ### Option 2: Run tests in VS Code Install recommended test explorer extensions and run unit tests through tool. **⚠️ Note:** For Windows, be sure to select the [virtual environment Python interpreter](https://docs.microsoft.com/en-us/visualstudio/python/installing-python-interpreters). ### Option 3: Run test command ``` make test ``` OR ``` poetry run python -m pytest /tests ``` ================================================ FILE: docs/Design_and_Architecture.md ================================================ # Design & Architecture This describes the design and architecture of the `electionguard-python` project. ## Design ### ✅ Simplicity Simplicity is the first and foremost goal of the code. The intent is for others to be able to **easily transliterate the code to any other programming language** with little more than structures and functions. This simplicity applies to all aspects of the code design, including naming. ### ✅ Extendable and Interpretable The library is intentionally general-purpose to support the different use cases of "end to end verifiable" voting systems. Different projects may wish to use different layers of the library, including math primitives, encryption functions, and more. ### ✅ Object Oriented Design (OOD) & Functional Methods An additional goal is to build a familiar object oriented design with underlying functional style methods. This allows users to simply construct objects in an OOP fashion or directly call the underlying methods in a functional way. This design also facilitates easy testing and composition. Class methods are used for simplicity, but sophistication with regard to inheritance, object encapsulation, or design patterns is intentionally avoided. These class methods usually rely on the aforementioned functional methods unless the class contains state. ### ✅ Immutable The library prefers immutable objects where possible to encourage simple data structures. ### ✅ Dataclasses `dataclass` is used frequently to simplify constructors. This follows the simplicity aspect, but also ensures easier serialization without being prescriptive on which library to use. ### ✅ Concurrency While this library is not explicitly engineered to _use_ concurrency, it's definitely meant to work properly when the caller wants to run more than one thing at a time. This means there is no global, mutable state in the library with the exception of a discrete-log function doing internal memoization, itself explicitly written to be thread-safe. ### ✅ Union Classes For both naming purposes and usability, union classes are generally preferred. This can alleviate issues with [multiple inheritance](#multiple-inheritance) ### 🚫 Exceptions To allow for easier transliteration, the library will not raise exception across the API boundary since this is not available in all languages. Instead, the library will have a variety of functions that indicate failures by returning `None`; the caller is expected to check if the result is `None` before any further processing. Python 3 `typing` calls this sort of result `Optional`.This tactic also indicates all exceptions raised are expected to be from bugs. ### 🚫 Multiple Inheritance Although a handy python feature, for implementation simplicity this feature is not used and should be avoided. ## Architecture ### 🤝 Approachable The python setup is designed to be as approachable as possible from the environment to the continuous integration. #### Setup The library contains a `Pipfile` that can be used with `pipenv`to ensure the correct dependencies are included, as well as a `setup.py` to install the package itself. There is also a `Makefile` which allows for simple `make` commands to ease new developers into the build process. If the user is a new developer, the recommendation is starting with [Visual Studio Code](https://code.visualstudio.com/) since there are many default settings and recommended extensions in the repository. #### Folder Structure The folder structure is kept to a bare minimum. The ElectionGuard library is located in `src/electionguard` and tests are in `tests`. Standalone applications or other pieces should be in separate subdirectories. For example, the `tests/bench` directory contains a simple Chaum-Pedersen proof computation benchmark. #### Commands To simplify the command structure, [make](https://www.gnu.org/software/make/manual/make.html) is used. A `Makefile` sits in the root directory and contains useful commands that can be used to run setup. These are shown in use in the [continuous integration](#continuous-integration). ### 🧹 Clean Code The library uses several tools to assist developers in maintaining clean code. Visual Studio Code is recommended for easier setup. #### Typing The library uses Python 3 **type hints** throughout and ensures return types are defined. **[Mypy](https://mypy.readthedocs.io/en/stable/)** is used to statically check the typing. #### Linting [Pylint]() is used for typing; settings are in the `.pylinrc` file. #### Formatting [Black]() is used for auto-formatting and checking the formatting of the python code. Settings are in the `pyproject.toml` file. ### 🧪 Testing The goal of the project is 100% code coverage with an understanding that there are some limitations. #### Property Based Property testing is helpful for [testing certain properties](https://fsharpforfunandprofit.com/posts/property-based-testing-2/). The library uses [Hypothesis](https://hypothesis.readthedocs.io/en/stable/) property-based testing to vigorously exercise the library. The library includes generator functions for all the core datatypes, making them easy to randomly generate. ### 🚀 Continuous Integration GitHub Actions are being used for continuous integration. Cross-platform is a primary goal and the workflows provided demonstrate how a developer can build in Linux, MacOS, and Windows. The run workflows can be seen on the GitHub repo page or a user can navigate to `.github/workflows` to inspect them. ### 📦 Math Library [gmpy2](https://gmpy2.readthedocs.io/en/latest/index.html) is a multiprecision numeric library that was chosen over Python's built-ins `int` type for its speed necessary for encryption performance. The current version used is `2.0.8` which is the most stable version. This is necessary for cross-platform since the library uses precompiled libraries for Windows due to reliance on `gmp`. The gmpy2 options are chosen over the native Python equivalents as shown below. - **Integers:** `int` -> [`mpz`](https://gmpy2.readthedocs.io/en/latest/mpz.html) - **Exponents:** `pow` -> `powmod` With the use of **mypy** for typing and the lack of type presence in [Typeshed](https://github.com/python/typeshed) for gmpy2, the library provides a stub in `stubs/gmpy2.pyi` to ensure the code compiles without warnings. ================================================ FILE: docs/Election_Manifest.md ================================================ # Overview There are many types of elections. We need a base set of data that shows how these different types of elections are handled in an ElectionGuard end-to-end verifiable election (or ballot comparison audits). We worked with InfernoRed, VotingWorks, and Dan Wallach of Rice University (thanks folks!) to develop a set of conventions, tests, and sample data (based on a starting dataset sample from the Center for Civic Design) that demonstrate how to encode the information necessary to conduct an election into a format that ElectionGuard can use. The election terms and structure are based whenever possible on the [NIST SP-1500-101 Election Event Logging Common Data Format Specification](https://pages.nist.gov/ElectionEventLogging/index.html) (with a prettier and (mostly) more functional implementation [here](https://developers.google.com/elections-data/reference) and a [PDF for version 2](https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.1500-100r2.pdf)). The information captured by the NIST standard is codified into an `election manifest` that defines common elements when conducting an election, such as locations, candidates, parties, contests, and ballot styles. ElectionGuard uses the data contained in the Election Manifest to associate ballots with specific ballot styles and to verify the accuracy of data at different stages of the election process. Note that not all of the data contained in the Election Manifest impacts the computations of tallies and zero-knowledge proofs used in the published election data that demonstrates end-to-end verifiability; however it is important to include as much data as possible in order to distinguish one election from another. With a well-defined Election Manifest, improperly formatted ballot encryption requests will fail with error messages at the moment of initial encryption; the enforcement of any logic or behavior to prevent overvoting or other malformed ballot submissions are handled by the encrypting device, not ElectionGuard. In addition, since json files do not accommodate comments, all notations and exceptions are documented in this readme. ## Election Data Structure [Elections are characterized into types by NIST](https://developers.google.com/elections-data/reference/election-type) as shown in the table below election type | description ------------- | ---------------- general | For an election held typically on the national day for elections. partisan-primary-closed | For a primary election that is for a specific party where voter eligibility is based on registration. partisan-primary-open | For a primary election that is for a specific party where voter declares desired party or chooses in private. primary | For a primary election without a specified type, such as a nonpartisan primary. runoff | For an election to decide a prior contest that ended with no candidate receiving a majority of the votes. special | For an election held out of sequence for special circumstances, for example, to fill a vacated office. other | Used when the election type is not listed in this enumeration. If used, include a specific value of the OtherType element. We present two sample manifests: `general` and `partisan-primary-closed`. The core distinction between the two samples is the role of party: in general elections voters can choose to vote for candidates from any party in a contest, regardless of party affiliation. In partisan primaries voters can only vote in contests germane to their party declaration or affiliation. As such, `special`, `runoff`, and `primary` election types will follow the `general` pattern, and `partisan-primary-open` will follow the `partisan-primary-closed` pattern. Open `primary` elections can follow either pattern as determined by their governing rules and regulations. (As noted above, ElectionGuard expects properly-formed ballots; e.g., it would error and fail to encrypt a ballot in an `open-primary-closed` election if a contest with an incorrect party affiliation were submitted (as indicated by the ).) ## Ballot Styles and Geography At least in the United States, many complications are introduced by voting simultaneously on election contests that apply in specific geographies and jurisdictions. For example, a single election could include contests for congress, state assembly, school, and utility districts, each with their own geographic boundaries, many that do not respect town or county lines. The ElectionGuard Election Manifest data format is flexible to accommodate most situations, but it is usually up to the election commission and the external system to determine what each component of the manifest actually means. In the following examples, we will work through the process of defining different election types at a high level and describe the process of building the election manifest. ### Geographic and Ballot Style Breakdown Each election can be thought of as a list of contests that are relevant to a certain group of people in a specific place. In order to determine who is supposed to vote on which contests, we first need to define the geographic jurisdictions where the election is taking place. [The NIST Guidelines](<https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.1500-100r2.pdf>) present an excellent discussion of the geographic interplay of different contests. The diagram from page 12 is presented below. ![](https://res.cloudinary.com/electionguard/image/upload/v1586960923/nist-election-model-uml.png) As the diagram shows, congressional, state assembly, school district and other geographic boundaries project onto towns and municipalities in different ways. Elections manage this complexity by creating unique ballot _styles_ that present to voters only the contests that pertain to them. Different jurisdictions use terms such as wards, precincts, and districts to describe the areas of overlap that guide ballot style creation. We will use `precinct` but `ward` and `district` could be used instead. ### Contests, Candidates and Parties In most cases, a resident of a specific _precinct_ or location will expect to see a certain list of contests that are relevant to them. A contest is a specific collection of available choices (_selections_) from which the voter may choose some subset. For the ElectionGuard Election Manifest, each possible selection in a contest must be associated with a candidate, even for Referendum-style contests. If a contest also supports write-in values, then a write-in candidate is also defined. Candidates may also be associated with specific parties, but this is not required for all election types. ## Introducing Hamilton County, OZ To help disambiguate, let's explore an example. ### Geographic Jurisdictions Hamilton County includes 3 townships: LaCroix, Arlington, Harris. The town of LaCroix also has a utility district that comprises its own precinct for special referendums. Arlington has two distinct school districts. The county is also split into two congressional districts, district 5 and district 7. Harris township is entirely within Congressional District 5, but both LaCroix and Arlington are split between congressional districts 5 and 7. ![Hamilton County Electoral Map](https://res.cloudinary.com/electionguard/image/upload/v1593617785/hamilton-county-district-map_xxki0z.png) #### Building the Geographic Jurisdiction Mapping (Geopolitical Units) The Election Manifest includes an array of objects called `geopoliticalUnits` (a.k.a. _gpUnit_). Each _Geopolitical Unit_ must include the following fields: - **objectId** - a unique identifier for the gpUnit. This value is used to map a contest to a specific jurisdiction - **name** - the friendly name of the gp Unit - **type** - they _type_ of jurisdiction (one of the [Reporting Unit Types](https://developers.google.com/elections-data/reference/reporting-unit-type)) - **contact information** - the contact info for the geopolitical unit Geopolitical units are polygons on a map represented by legal jurisdictions. In our example Election Manifest for hamilton County, there is one geopolitical unit for each jurisdictional boundary in the image above: - Hamilton County - Congressional District 5 - congressional District 7 - LaCroix Township - Exeter Utility District (within LaCroix Township) - Harris Township - Arlington Township - Pismo Beach School District (within Arlington Township) - Somerset School District (within Arlington Township) When defining the geopolitical units for an election, we define all of the possible geopolitical units for an election; even if there are no contests for a specific jurisdiction. This way, if contests are added or removed during the setup phase, you do not also have to remember to update the list of geopolitical units. Alternatively, you can define only the GP Units for which there are contests. ### The General Election Contests A **general election** will occur in Hamilton County. The county is voting along with the rest of the province, and the county is responsible for tabulating its own election results. This means that the _Election Scope_ is defined at the county level. For the `general` election, the following sets of contests (and associated geographic boundaries) obtain: 1. **The National Contests** - President and Vice President. This contest demonstrates a "vote for the ticket" and allows write-ins 2. **Province Contests** - Governor - this contest demonstrates a long list of candidate names 3. **Congressional Contests** - Congress Districts 5 and 7 - these contests demonstrate how to split a district using multiple ballot styles 4. **Township Contests** - Retain Chief Justice - This contest demonstrates a contest that applies to a specific town whose boundaries are split across multiple ballot styles 4. **School District Contests** - School Board - these contests demonstrate contests with multiple selections (_n-of-m_) and allow write-ins School Board, and Utility district referendum to show ballot style splits 5. **Utility District Contest** - Utility District - This contest demonstrates a referendum-style contest with long descriptions and display language translation into Spanish Each contest must be associated with exactly **ONE** `electoralDistrictId`. The `electoralDistrictId` field on the contest is populated with the `objectId` of the associated _Geopolitical Unit_ (e.g. the Contest `congress-district-7-contest` has the `electoralDistrictId` `congress-district-7` Each contest must also define a `sequenceOrder`. the _sequence order_ is an indexing mechanism used internally. _It is not the sequence that the contests are displayed to the user_. The order in which contests are displayed to the user is up to the implementing application. ### The General Election Ballot Styles A ballot style is the set of contests that a specific voter should see on their ballot for a given location. The ballot style is associated to the set of geopolitical units relevant to a specific point on a map. Since each contest is also associated with a geopolitical unit, a mapping is created between a point on a map and the contests that are relevant to that point. For instance, a voter that lives in the _Exeter Utility District_ should see contests that are relevant to Congressional District 7, LaCroix Township and the Exeter Utility District. | Geopolitical Units are overlapping polygons, and ballot styles are the list of polygons relevant to a specific point on the map. Similar to Geopolitical Units, we define all of the possible ballot styles for an election in our example, even if there are no contests specific to a ballot style. This is subjective and the behavior may be different for the integrating system: - Congressional District 7 Outside Any Township - Congressional District 7 LaCroix Township - Congressional District 7 LaCroix Township Exeter Utility District - Congressional District 7 Arlington Township - Congressional District 7 Arlington Township Pismo Beach School district - Congressional District 7 Arlington Township Somerset School district - Congressional District 5 Outside Any Township - Congressional District 5 LaCroix Township - Congressional District 5 Harris Township - Congressional District 5 Arlington Township Pismo Beach School district - Congressional District 5 Arlington Township Somerset School district By defining all of the possible ballot styles and all of the possible geopolitical units, we ensure that if a contest is added or removed, we only have to make sure the contest is correct. We do not have to modify the list of geopolitical units or ballot styles. ## Data Flexibility The relationship between a ballot style and the contests that are displayed on it are subjective to the implementing application. This example is just one way to define this relationship that is purposefully verbose. For instance, in our example we define a geopolitical unit as a set of overlapping polygons, and a ballot style as the intersection of those polygons at a specific point. This is a top-down approach. Alternatively, we could have defined a geopolitical unit as the intersection area of those polygons and mapped one ballot style to each geopolitical unit 1 to 1. for instance, instead of defining a single GP Unit each for: - Congressional District 5, - Congressional District 7, - LaCroix Township, - Exeter Utility district, etc; we could have instead defined the GP Units as: - Congressional District 5 No Township - Congressional District 7 No Township - Congressional District 5 inside LaCroix - Congressional District 5 Inside LaCroix and Exeter, etc. Then, instead of each Ballot Style having multiple GP Units, each ballot style would have applied to exactly one GP Unit. ### Data Validation When the election Manifest is loaded into ElectionGuard, its validity is checked semantically against the data format required to conduct an ElectionGuard Election. Specifically, we check that: - Each Geopolitical Unit has a unique objectId - Each Ballot Style maps to at least one valid Geopolitical Unit - Each Party has a unique objectId - Each Candidate either does not have a party, or is associated with a valid party - Each Contest has a unique Sequence Order - Each contest is associated with exactly one valid geopolitical unit - Each contest has a valid number of selections for the number of seats in the contest - Each selection on each contest is associated with a valid Candidate as long as the election manifest format matches the validation criteria, the election can proceed as an ElectionGuard election. ## Frequently Asked Questions Q: What if my ballot styles are not associated with geopolitical units? A: There are a few ways to handle this. In most cases, you can simply map the ballot style 1 to 1 to the geopolitical unit. for instance, if `ballot-style-1` includes `contest-1` then you may create `geopolitical-unit-1` and associate both the ballot style and the contest to that geopolitical unit. This documentation is under review and subject to change. Please do not hesitate to open a github issue if you have questions, or find errors or omissions. ================================================ FILE: docs/Project_Workflow.md ================================================ # Project Workflow ## ✨ Start an Iteration Each iteration on this repository will be tracked by a GitHub **[Milestone](https://help.github.com/en/github/managing-your-work-on-github/about-milestones)**. The completion of the milestone will queue a GitHub **[Release](https://help.github.com/en/github/administering-a-repository/managing-releases-in-a-repository)**. Issues will be added to the milestones to indicate work needed. After completion, this milestone can then act as a list of work contained within a release. ## 🔀 Pull Request ### Attach Issue Each pull request **MUST** be attached to an issue. On the surface, this ensures that closing a pull request will close an issue. This also ensures the issue can be included in the milestone. For this repository, the use of issues assists the team to use the [project board]() to track the progress towards a milestone. ### Validation Each pull request is validated by the [Pull Request Validation](https://github.com/microsoft/electionguard-python/blob/main/.github/workflows/pull_request.yml) GitHub [Action](https://help.github.com/en/actions). This action can be viewed from the PR or from the actions to inspect the details. ### Review All pull requests require a review from a **Contributor** but any reviewers are welcome. ## 🏁 Create a Release At the end of an iteration aka when a milestone is complete, a release can be created. ### Steps 1. Raise version number in `setup.py` 2. Close Milestone 3. Edit Release details _(optional)_ ### Release Workflow Closing the milestone queues the [Release Build](https://github.com/microsoft/electionguard-python/blob/main/.github/workflows/release.yml) GitHub [Action](https://help.github.com/en/actions). This action is designed to reduce the effort by maintainers and give the community an open view of the package flow. - Build package - Create dependency graph - Upload package to PyPi - Validate PyPi package - Upload package and graph to GitHub Workflow - Create Release - Upload zipped package and graph to Release - Update GitHub Pages Documentation ================================================ FILE: docs/Tablet Setup.md ================================================ # Setup Surface Go 3 Tablet This documentation is a reference on how to setup a brand new computer (in this case a Surface Go 3) to use the Python repository to run the Admin and Guardian GUIs for an election. Some steps, such as "Turn Off S Mode" might not apply to the computer you are setting up. All usernames and passwords are set to xxx in this document and should be set to valid values for your own purpose. ## Setup Windows * When prompted for email address use * xxxx@xxxx.com * xxxx * skip protecting account * skip hello setup * pin xxxx * default privacy * skip customization * decline office 365 * installs Win11 (30 min or so) ## Turn Off S Mode * settings -> system -> activation * opens windows store * hit Get button and wait for it to complete ## Windows Updates * Do all the updates (a lot of updates) * Change to best performance mode * Settings -> System -> Power and Battery * Power Mode -> Best Performance ## Install Hyper-V and Linux (admin only) * Go to Settings * Search for features * Go to optional features * More Windows Features * Install WSL, Virtual Machine Platform and Windows Hypervisor Platform settings * Restart windows * Go to windows store and install ubuntu * When running it the first time it will give a link to install a new kernel for WSL2 * Run ubuntu and setup a user * Username: xxxx * Password: xxxx ## Docker Installation (admin only) * Get docker desktop and install. * In the settings (gear icon) make the following changes: * Make sure that "Start Docker Desktop when you log in" is on * Make sure that "Use the WSL 2 based engine" is on ## Developer Tools * Command prompt -> python3 (install from windows store) * Install chrome (does not need to be set to current browser) * Install VS Code * Git * https://git-scm.com/download/win * Install chocolatey using powershell command from https://chocolatey.org/install * Install make * choco install make * Install poetry (powershell) https://python-poetry.org/docs * Add to path (See "Set Environment Variables" below on steps to get to the path) ## Download Python Source Code * Open a Command prompt and use the following commands * mkdir code * cd code * git clone https://github.com/microsoft/electionguard-python ## Terminal Settings * Open up Terminal * Go to settings in Terminal * Default Profile => Command Prompt * Profiles (left) -> Defaults * Run this profile as Admin -> on * Starting Starting directory to be directory where source code is downloaded * Hit "Save" button at the bottom of the window ## User Interface Changes * Change touch keyboard to traditional instead of default * Go into resize (using the gear icon) and set the zoom to 200 (max value) ## Set Environment Variables * Go to Settings * Search for environment * Select "Edit the system environment variables" * Select the button "Environment Variables" * Select "New…" * Create the following settings * EG_DB_PASSWORD = xxxx * EG_DB_HOST = 10.10.0.100 * EG_IS_ADMIN = true for an admin and false if guardian * admin only - EG_DB_DIR = ./database ## Set Python Code * Open Terminal and run the following commands * make environment * There will be an error at the end. This is normal * poetry run eg * should show the help for the eg command ================================================ FILE: docs/index.md ================================================ ![Microsoft Defending Democracy Program: ElectionGuard Python](https://github.com/microsoft/electionguard-python/blob/main/images/electionguard-banner.svg?raw=true) # 🗳 ElectionGuard Python [![ElectionGuard Specification 0.95.0](https://img.shields.io/badge/🗳%20ElectionGuard%20Specification-0.95.0-green)](https://www.electionguard.vote) ![Github Package Action](https://github.com/microsoft/electionguard-python/workflows/Release%20Build/badge.svg) [![](https://img.shields.io/pypi/v/electionguard)](https://pypi.org/project/electionguard/) [![](https://img.shields.io/pypi/dm/electionguard)](https://pypi.org/project/electionguard/) [![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/microsoft/electionguard-python.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/microsoft/electionguard-python/context:python) [![Total alerts](https://img.shields.io/lgtm/alerts/g/microsoft/electionguard-python.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/microsoft/electionguard-python/alerts/) [![Documentation Status](https://readthedocs.org/projects/electionguard-python/badge/?version=latest)](https://electionguard-python.readthedocs.io) [![license](https://img.shields.io/github/license/microsoft/electionguard)](https://github.com/microsoft/electionguard-python/blob/main/LICENSE) This repository is a "reference implementation" of ElectionGuard written in Python 3. This implementation can be used to conduct End-to-End Verifiable Elections as well as privacy-enhanced risk-limiting audits. Components of this library can also be used to construct "Verifiers" to validate the results of an ElectionGuard election. ## 📁 In This Repository | File/folder | Description | | --------------------------- | --------------------------------------------- | | docs | Documentation for using the library. | | src/electionguard | ElectionGuard library. | | src/electionguard_tools | Tools for testing and sample data. | | src/electionguard_verifier | Verifier to validate the validity of a ballot.| | stubs | Type annotations for external libraries. | | tests | Tests to exercise this codebase. | | CONTRIBUTING.md | Guidelines for contributing. | | README.md | This README file. | | LICENSE | The license for ElectionGuard-Python. | | data | Sample election data. | <br/> ## ❓ What Is ElectionGuard? ElectionGuard is an open source software development kit (SDK) that makes voting more secure, transparent and accessible. The ElectionGuard SDK leverages homomorphic encryption to ensure that votes recorded by electronic systems of any type remain encrypted, secure, and secret. Meanwhile, ElectionGuard also allows verifiable and accurate tallying of ballots by any 3rd party organization without compromising secrecy or security. Learn More in the [ElectionGuard Repository](https://github.com/microsoft/electionguard) ## 🦸 How Can I use ElectionGuard? ElectionGuard supports a variety of use cases. The Primary use case is to generate verifiable end-to-end (E2E) encrypted elections. The ElectionGuard process can also be used for other use cases such as privacy enhanced risk-limiting audits (RLAs). ## 💻 Requirements - [Python 3.9+](https://www.python.org/downloads/) is <ins>**required**</ins> to develop this SDK. If developer uses multiple versions of python, [pyenv](https://github.com/pyenv/pyenv) is suggested to assist version management. - [GNU Make](https://www.gnu.org/software/make/manual/make.html) is used to simplify the commands and GitHub Actions. This approach is recommended to simplify the command line experience. This is built in for MacOS and Linux. For Windows, setup is simpler with [Chocolatey](https://chocolatey.org/install) and installing the provided [make package](https://chocolatey.org/packages/make). The other Windows option is [manually installing make](http://gnuwin32.sourceforge.net/packages/make.htm). - [Gmpy2](https://gmpy2.readthedocs.io/en/latest/) is used for [Arbitrary-precision arithmetic](https://en.wikipedia.org/wiki/Arbitrary-precision_arithmetic) which has its own [installation requirements (native C libraries)](https://gmpy2.readthedocs.io/en/latest/intro.html#installation) on Linux and MacOS. **⚠️ Note:** _This is not required for Windows since the gmpy2 precompiled libraries are provided._ - [poetry 2.2.1](https://python-poetry.org/) is used to configure the python environment. Installation instructions can be found [here](https://python-poetry.org/docs/#installation). ## 🚀 Quick Start Using [**make**](https://www.gnu.org/software/make/manual/make.html), the entire [GitHub Action workflow][pull request workflow] can be run with one command: ``` make ``` The unit and integration tests can also be run with make: ``` make test ``` A complete end-to-end election example can be run independently by executing: ``` make test-example ``` For more detailed build and run options, see the [documentation](Build_and_Run.md). ## 📄 Documentation Sections: - [Design and Architecture](Design_and_Architecture.md) - [Build and Run](Build_and_Run.md) - [Project Workflow](Project_Workflow.md) - [Election Manifest](Election_Manifest.md) Step-by-Step Process: 0. [Configure Election](0_Configure_Election.ipynb) 1. [Key Ceremony](1_Key_Ceremony.md) 2. [Encrypt Ballots](2_Encrypt_Ballots.md) 3. [Cast and Spoil](3_Cast_and_Spoil.md) 4. [Decrypt Tally](4_Decrypt_Tally.md) 5. [Publish and Verify](5_Publish_and_Verify.md) ## ❓Questions Electionguard would love for you to ask questions out in the open using GitHub Issues. If you really want to email the ElectionGuard team, reach out at electionguard@microsoft.com. ================================================ FILE: mkdocs.yml ================================================ site_name: ElectionGuard Python site_url: https://electionguard-python.readthedocs.io/ # site_description: site_author: Microsoft # google_analytics: # remote_branch: for gh-deploy to GithubPages # remote_name: for gh-deploy to Github Pages copyright: "© Microsoft 2020" docs_dir: "docs" repo_url: https://github.com/microsoft/electionguard-python/ nav: - Home: index.md - Design and Architecture: Design_and_Architecture.md - Build and Run: Build_and_Run.md - Project Workflow: Project_Workflow.md - Election Manifest: Election_Manifest.md - Steps: - 0. Configure Election: 0_Configure_Election.ipynb - 1. Key Ceremony: 1_Key_Ceremony.md - 2. Encrypt Ballots: 2_Encrypt_Ballots.md - 3. Cast and Spoil: 3_Cast_and_Spoil.md - 4. Decrypt Tally: 4_Decrypt_Tally.md - 5. Publish and Verify: 5_Publish_and_Verify.md theme: readthedocs plugins: - mkdocs-jupyter markdown_extensions: - admonition - pymdownx.details - pymdownx.superfences ================================================ FILE: pyproject.toml ================================================ [project] name = "electionguard" version = "1.4.0" requires-python = ">=3.9.5,<4.0.0" description = "ElectionGuard: Support for e2e verified elections." license = "MIT" authors = [{"name" = "Microsoft", "email" = "electionguard@microsoft.com"}] maintainers = [] readme = "README.md" keywords = [] classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Operating System :: Unix", "Operating System :: POSIX", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Programming Language :: Python", "Topic :: Utilities" ] dependencies = [ "gmpy2>=2.0.8,<3.0.0", "psutil>=5.7.2", "pydantic==2.13.0", "click>=8.1.0,<9.0.0", "dacite>=1.6.0,<2.0.0", "python-dateutil>=2.8.2,<3.0.0", "types-python-dateutil>=2.8.14,<3.0.0", "Eel[jinja2]>=0.14.0,<1.0.0", "pymongo>=4.1.1,<5.0.0", "dependency-injector>=4.39.1,<5.0.0", "pytest-mock>=3.8.2,<4.0.0", ] [project.urls] homepage = "https://microsoft.github.io/electionguard-python" repository = "https://github.com/microsoft/electionguard-python" documentation = "https://microsoft.github.io/electionguard-python" "GitHub Pages" = "https://microsoft.github.io/electionguard-python" "Read the Docs" = "https://electionguard-python.readthedocs.io" "Releases" = "https://github.com/microsoft/electionguard-python/releases" "Milestones" = "https://github.com/microsoft/electionguard-python/milestones" "Issue Tracker" = "https://github.com/microsoft/electionguard-python/issues" [tool.poetry] packages = [ { include = "electionguard", from = "src" }, { include = "electionguard_tools", from = "src" }, { include = "electionguard_cli", from = "src" }, { include = "electionguard_gui", from = "src" }, ] [tool.poetry.group.dev.dependencies] atomicwrites = "*" black = "25.11.0" coverage = "*" docutils = "*" hypothesis = ">=5.15.1" ipython = "^7.31.1" ipykernel = "^6.4.1" jeepney = "*" jupyter-black = "^0.3.1" mkdocs = "^1.6.1" mkdocs-jupyter = "^0.26.2" mkinit = "^0.3.3" mypy = "1.19.1" pydeps = "*" pylint = "*" pytest = "*" secretstorage = "*" twine = "*" typish = '*' [project.scripts] eg = 'electionguard_cli.start:cli' egui = 'electionguard_gui.start:run' [tool.black] target-version = ['py39'] [tool.pylint.basic] extension-pkg-whitelist = "pydantic" [tool.pylint.format] max-line-length = 120 # FIXME: Pylint should not require this many exceptions [tool.pylint.messages_control] disable = ''' duplicate-code, fixme, invalid-name, missing-module-docstring, missing-function-docstring, no-value-for-parameter, redefined-builtin, broad-exception-raised, too-few-public-methods, too-many-arguments, too-many-branches, too-many-function-args, too-many-lines, too-many-locals, too-many-nested-blocks, too-many-positional-arguments, unnecessary-lambda, ''' [tool.coverage.run] branch = true source = ["src/electionguard"] [tool.coverage.html] directory = "coverage_html_report" [build-system] requires = ["poetry-core>=2.0.0"] build-backend = "poetry.core.masonry.api" [tool.mypy] python_version = "3.9" warn_return_any = true warn_unused_configs = true disallow_untyped_defs = true ignore_missing_imports = true show_column_numbers = true ================================================ FILE: src/electionguard/__init__.py ================================================ import importlib.metadata # <AUTOGEN_INIT> from electionguard import ballot from electionguard import ballot_box from electionguard import ballot_code from electionguard import ballot_compact from electionguard import ballot_validator from electionguard import big_integer from electionguard import byte_padding from electionguard import chaum_pedersen from electionguard import constants from electionguard import data_store from electionguard import decrypt_with_secrets from electionguard import decrypt_with_shares from electionguard import decryption from electionguard import decryption_mediator from electionguard import decryption_share from electionguard import discrete_log from electionguard import election from electionguard import election_object_base from electionguard import election_polynomial from electionguard import elgamal from electionguard import encrypt from electionguard import group from electionguard import guardian from electionguard import hash from electionguard import hmac from electionguard import key_ceremony from electionguard import key_ceremony_mediator from electionguard import logs from electionguard import manifest from electionguard import nonces from electionguard import proof from electionguard import scheduler from electionguard import schnorr from electionguard import serialize from electionguard import singleton from electionguard import tally from electionguard import type from electionguard import utils from electionguard.ballot import ( BallotBoxState, CiphertextBallot, CiphertextBallotContest, CiphertextBallotSelection, CiphertextContest, CiphertextSelection, PlaintextBallot, PlaintextBallotContest, PlaintextBallotSelection, SubmittedBallot, create_ballot_hash, make_ciphertext_ballot, make_ciphertext_ballot_contest, make_ciphertext_ballot_selection, make_ciphertext_submitted_ballot, ) from electionguard.ballot_box import ( BallotBox, cast_ballot, get_ballots, spoil_ballot, submit_ballot, submit_ballot_to_box, ) from electionguard.ballot_code import ( get_ballot_code, get_hash_for_device, ) from electionguard.ballot_compact import ( CompactPlaintextBallot, CompactSubmittedBallot, NO_VOTE, YES_VOTE, compress_plaintext_ballot, compress_submitted_ballot, expand_compact_plaintext_ballot, expand_compact_submitted_ballot, ) from electionguard.ballot_validator import ( ballot_is_valid_for_election, ballot_is_valid_for_style, contest_is_valid_for_style, selection_is_valid_for_style, ) from electionguard.big_integer import ( BigInteger, bytes_to_hex, ) from electionguard.byte_padding import ( DataSize, TruncationError, add_padding, remove_padding, to_padded_bytes, ) from electionguard.chaum_pedersen import ( ChaumPedersenProof, ConstantChaumPedersenProof, DisjunctiveChaumPedersenProof, make_chaum_pedersen, make_constant_chaum_pedersen, make_disjunctive_chaum_pedersen, make_disjunctive_chaum_pedersen_one, make_disjunctive_chaum_pedersen_zero, ) from electionguard.constants import ( EXTRA_SMALL_TEST_CONSTANTS, ElectionConstants, LARGE_TEST_CONSTANTS, MEDIUM_TEST_CONSTANTS, PrimeOption, SMALL_TEST_CONSTANTS, STANDARD_CONSTANTS, create_constants, get_cofactor, get_constants, get_generator, get_large_prime, get_small_prime, ) from electionguard.data_store import ( DataStore, ReadOnlyDataStore, ) from electionguard.decrypt_with_secrets import ( decrypt_ballot_with_nonce, decrypt_ballot_with_secret, decrypt_contest_with_nonce, decrypt_contest_with_secret, decrypt_selection_with_nonce, decrypt_selection_with_secret, ) from electionguard.decrypt_with_shares import ( decrypt_ballot, decrypt_contest_with_decryption_shares, decrypt_selection_with_decryption_shares, decrypt_tally, ) from electionguard.decryption import ( RecoveryPublicKey, compute_compensated_decryption_share, compute_compensated_decryption_share_for_ballot, compute_compensated_decryption_share_for_contest, compute_compensated_decryption_share_for_selection, compute_decryption_share, compute_decryption_share_for_ballot, compute_decryption_share_for_contest, compute_decryption_share_for_selection, compute_lagrange_coefficients_for_guardian, compute_lagrange_coefficients_for_guardians, compute_recovery_public_key, decrypt_backup, decrypt_with_threshold, partially_decrypt, reconstruct_decryption_contest, reconstruct_decryption_share, reconstruct_decryption_share_for_ballot, ) from electionguard.decryption_mediator import ( DecryptionMediator, ) from electionguard.decryption_share import ( CiphertextCompensatedDecryptionContest, CiphertextCompensatedDecryptionSelection, CiphertextDecryptionContest, CiphertextDecryptionSelection, CompensatedDecryptionShare, DecryptionShare, ProofOrRecovery, create_ciphertext_decryption_selection, get_shares_for_selection, ) from electionguard.discrete_log import ( DiscreteLog, DiscreteLogCache, DiscreteLogExponentError, DiscreteLogNotFoundError, compute_discrete_log, compute_discrete_log_async, compute_discrete_log_cache, precompute_discrete_log_cache, ) from electionguard.election import ( CiphertextElectionContext, Configuration, make_ciphertext_election_context, ) from electionguard.election_object_base import ( ElectionObjectBase, OrderedObjectBase, list_eq, sequence_order_sort, ) from electionguard.election_polynomial import ( Coefficient, ElectionPolynomial, LagrangeCoefficientsRecord, PublicCommitment, SecretCoefficient, compute_lagrange_coefficient, compute_polynomial_coordinate, generate_polynomial, verify_polynomial_coordinate, ) from electionguard.elgamal import ( ElGamalCiphertext, ElGamalKeyPair, ElGamalPublicKey, ElGamalSecretKey, HashedElGamalCiphertext, elgamal_add, elgamal_combine_public_keys, elgamal_encrypt, elgamal_keypair_from_secret, elgamal_keypair_random, hashed_elgamal_encrypt, ) from electionguard.encrypt import ( ContestData, EncryptionDevice, EncryptionMediator, contest_from, encrypt_ballot, encrypt_ballot_contests, encrypt_contest, encrypt_selection, generate_device_uuid, selection_from, ) from electionguard.group import ( BaseElement, ElementModP, ElementModPOrQ, ElementModPOrQorInt, ElementModPorInt, ElementModQ, ElementModQorInt, a_minus_b_q, a_plus_bc_q, add_q, div_p, div_q, g_pow_p, hex_to_p, hex_to_q, int_to_p, int_to_q, mult_inv_p, mult_p, mult_q, negate_q, pow_p, pow_q, rand_q, rand_range_q, ) from electionguard.guardian import ( Guardian, GuardianRecord, PrivateGuardianRecord, get_valid_ballot_shares, publish_guardian_record, ) from electionguard.hash import ( CryptoHashCheckable, CryptoHashable, CryptoHashableAll, CryptoHashableT, hash_elems, ) from electionguard.hmac import ( get_hmac, ) from electionguard.key_ceremony import ( CeremonyDetails, CoordinateData, ElectionJointKey, ElectionKeyPair, ElectionPartialKeyBackup, ElectionPartialKeyChallenge, ElectionPartialKeyVerification, ElectionPublicKey, combine_election_public_keys, generate_election_key_pair, generate_election_partial_key_backup, generate_election_partial_key_challenge, get_backup_seed, verify_election_partial_key_backup, verify_election_partial_key_challenge, ) from electionguard.key_ceremony_mediator import ( BackupVerificationState, GuardianPair, KeyCeremonyMediator, ) from electionguard.logs import ( ElectionGuardLog, FORMAT, LOG, get_file_handler, get_stream_handler, log_add_handler, log_critical, log_debug, log_error, log_handlers, log_info, log_remove_handler, log_warning, ) from electionguard.manifest import ( AnnotatedString, BallotStyle, Candidate, CandidateContestDescription, ContactInformation, ContestDescription, ContestDescriptionWithPlaceholders, ElectionType, GeopoliticalUnit, InternalManifest, InternationalizedText, Language, Manifest, Party, ReferendumContestDescription, ReportingUnitType, SUPPORTED_VOTE_VARIATIONS, SelectionDescription, SpecVersion, VoteVariationType, contest_description_with_placeholders_from, generate_placeholder_selection_from, generate_placeholder_selections_from, get_i8n_value, ) from electionguard.nonces import ( Nonces, ) from electionguard.proof import ( Proof, ProofUsage, ) from electionguard.scheduler import ( Scheduler, ) from electionguard.schnorr import ( SchnorrProof, make_schnorr_proof, ) from electionguard.serialize import ( construct_path, from_file, from_file_wrapper, from_list_in_file, from_list_in_file_wrapper, from_list_raw, from_raw, get_schema, padded_decode, padded_encode, to_file, to_raw, ) from electionguard.singleton import ( Singleton, ) from electionguard.tally import ( CiphertextTally, CiphertextTallyContest, CiphertextTallySelection, PlaintextTally, PlaintextTallyContest, PlaintextTallySelection, PublishedCiphertextTally, tally_ballot, tally_ballots, ) from electionguard.type import ( BallotId, ContestId, GuardianId, MediatorId, SelectionId, VerifierId, ) from electionguard.utils import ( BYTE_ENCODING, BYTE_ORDER, ContestErrorType, ContestException, NullVoteException, OverVoteException, UnderVoteException, flatmap_optional, get_optional, get_or_else_optional, get_or_else_optional_func, match_optional, space_between_capitals, to_hex_bytes, to_iso_date_string, to_ticks, ) __all__ = [ "AnnotatedString", "BYTE_ENCODING", "BYTE_ORDER", "BackupVerificationState", "BallotBox", "BallotBoxState", "BallotId", "BallotStyle", "BaseElement", "BigInteger", "Candidate", "CandidateContestDescription", "CeremonyDetails", "ChaumPedersenProof", "CiphertextBallot", "CiphertextBallotContest", "CiphertextBallotSelection", "CiphertextCompensatedDecryptionContest", "CiphertextCompensatedDecryptionSelection", "CiphertextContest", "CiphertextDecryptionContest", "CiphertextDecryptionSelection", "CiphertextElectionContext", "CiphertextSelection", "CiphertextTally", "CiphertextTallyContest", "CiphertextTallySelection", "Coefficient", "CompactPlaintextBallot", "CompactSubmittedBallot", "CompensatedDecryptionShare", "Configuration", "ConstantChaumPedersenProof", "ContactInformation", "ContestData", "ContestDescription", "ContestDescriptionWithPlaceholders", "ContestErrorType", "ContestException", "ContestId", "CoordinateData", "CryptoHashCheckable", "CryptoHashable", "CryptoHashableAll", "CryptoHashableT", "DataSize", "DataStore", "DecryptionMediator", "DecryptionShare", "DiscreteLog", "DiscreteLogCache", "DiscreteLogExponentError", "DiscreteLogNotFoundError", "DisjunctiveChaumPedersenProof", "EXTRA_SMALL_TEST_CONSTANTS", "ElGamalCiphertext", "ElGamalKeyPair", "ElGamalPublicKey", "ElGamalSecretKey", "ElectionConstants", "ElectionGuardLog", "ElectionJointKey", "ElectionKeyPair", "ElectionObjectBase", "ElectionPartialKeyBackup", "ElectionPartialKeyChallenge", "ElectionPartialKeyVerification", "ElectionPolynomial", "ElectionPublicKey", "ElectionType", "ElementModP", "ElementModPOrQ", "ElementModPOrQorInt", "ElementModPorInt", "ElementModQ", "ElementModQorInt", "EncryptionDevice", "EncryptionMediator", "FORMAT", "GeopoliticalUnit", "Guardian", "GuardianId", "GuardianPair", "GuardianRecord", "HashedElGamalCiphertext", "InternalManifest", "InternationalizedText", "KeyCeremonyMediator", "LARGE_TEST_CONSTANTS", "LOG", "LagrangeCoefficientsRecord", "Language", "MEDIUM_TEST_CONSTANTS", "Manifest", "MediatorId", "NO_VOTE", "Nonces", "NullVoteException", "OrderedObjectBase", "OverVoteException", "Party", "PlaintextBallot", "PlaintextBallotContest", "PlaintextBallotSelection", "PlaintextTally", "PlaintextTallyContest", "PlaintextTallySelection", "PrimeOption", "PrivateGuardianRecord", "Proof", "ProofOrRecovery", "ProofUsage", "PublicCommitment", "PublishedCiphertextTally", "ReadOnlyDataStore", "RecoveryPublicKey", "ReferendumContestDescription", "ReportingUnitType", "SMALL_TEST_CONSTANTS", "STANDARD_CONSTANTS", "SUPPORTED_VOTE_VARIATIONS", "Scheduler", "SchnorrProof", "SecretCoefficient", "SelectionDescription", "SelectionId", "Singleton", "SpecVersion", "SubmittedBallot", "TruncationError", "UnderVoteException", "VerifierId", "VoteVariationType", "YES_VOTE", "a_minus_b_q", "a_plus_bc_q", "add_padding", "add_q", "ballot", "ballot_box", "ballot_code", "ballot_compact", "ballot_is_valid_for_election", "ballot_is_valid_for_style", "ballot_validator", "big_integer", "byte_padding", "bytes_to_hex", "cast_ballot", "chaum_pedersen", "combine_election_public_keys", "compress_plaintext_ballot", "compress_submitted_ballot", "compute_compensated_decryption_share", "compute_compensated_decryption_share_for_ballot", "compute_compensated_decryption_share_for_contest", "compute_compensated_decryption_share_for_selection", "compute_decryption_share", "compute_decryption_share_for_ballot", "compute_decryption_share_for_contest", "compute_decryption_share_for_selection", "compute_discrete_log", "compute_discrete_log_async", "compute_discrete_log_cache", "compute_lagrange_coefficient", "compute_lagrange_coefficients_for_guardian", "compute_lagrange_coefficients_for_guardians", "compute_polynomial_coordinate", "compute_recovery_public_key", "constants", "construct_path", "contest_description_with_placeholders_from", "contest_from", "contest_is_valid_for_style", "create_ballot_hash", "create_ciphertext_decryption_selection", "create_constants", "data_store", "decrypt_backup", "decrypt_ballot", "decrypt_ballot_with_nonce", "decrypt_ballot_with_secret", "decrypt_contest_with_decryption_shares", "decrypt_contest_with_nonce", "decrypt_contest_with_secret", "decrypt_selection_with_decryption_shares", "decrypt_selection_with_nonce", "decrypt_selection_with_secret", "decrypt_tally", "decrypt_with_secrets", "decrypt_with_shares", "decrypt_with_threshold", "decryption", "decryption_mediator", "decryption_share", "discrete_log", "div_p", "div_q", "election", "election_object_base", "election_polynomial", "elgamal", "elgamal_add", "elgamal_combine_public_keys", "elgamal_encrypt", "elgamal_keypair_from_secret", "elgamal_keypair_random", "encrypt", "encrypt_ballot", "encrypt_ballot_contests", "encrypt_contest", "encrypt_selection", "expand_compact_plaintext_ballot", "expand_compact_submitted_ballot", "flatmap_optional", "from_file", "from_file_wrapper", "from_list_in_file", "from_list_in_file_wrapper", "from_list_raw", "from_raw", "g_pow_p", "generate_device_uuid", "generate_election_key_pair", "generate_election_partial_key_backup", "generate_election_partial_key_challenge", "generate_placeholder_selection_from", "generate_placeholder_selections_from", "generate_polynomial", "get_backup_seed", "get_ballot_code", "get_ballots", "get_cofactor", "get_constants", "get_file_handler", "get_generator", "get_hash_for_device", "get_hmac", "get_i8n_value", "get_large_prime", "get_optional", "get_or_else_optional", "get_or_else_optional_func", "get_schema", "get_shares_for_selection", "get_small_prime", "get_stream_handler", "get_valid_ballot_shares", "group", "guardian", "hash", "hash_elems", "hashed_elgamal_encrypt", "hex_to_p", "hex_to_q", "hmac", "int_to_p", "int_to_q", "key_ceremony", "key_ceremony_mediator", "list_eq", "log_add_handler", "log_critical", "log_debug", "log_error", "log_handlers", "log_info", "log_remove_handler", "log_warning", "logs", "make_chaum_pedersen", "make_ciphertext_ballot", "make_ciphertext_ballot_contest", "make_ciphertext_ballot_selection", "make_ciphertext_election_context", "make_ciphertext_submitted_ballot", "make_constant_chaum_pedersen", "make_disjunctive_chaum_pedersen", "make_disjunctive_chaum_pedersen_one", "make_disjunctive_chaum_pedersen_zero", "make_schnorr_proof", "manifest", "match_optional", "mult_inv_p", "mult_p", "mult_q", "negate_q", "nonces", "padded_decode", "padded_encode", "partially_decrypt", "pow_p", "pow_q", "precompute_discrete_log_cache", "proof", "publish_guardian_record", "rand_q", "rand_range_q", "reconstruct_decryption_contest", "reconstruct_decryption_share", "reconstruct_decryption_share_for_ballot", "remove_padding", "scheduler", "schnorr", "selection_from", "selection_is_valid_for_style", "sequence_order_sort", "serialize", "singleton", "space_between_capitals", "spoil_ballot", "submit_ballot", "submit_ballot_to_box", "tally", "tally_ballot", "tally_ballots", "to_file", "to_hex_bytes", "to_iso_date_string", "to_padded_bytes", "to_raw", "to_ticks", "type", "utils", "verify_election_partial_key_backup", "verify_election_partial_key_challenge", "verify_polynomial_coordinate", ] # </AUTOGEN_INIT> # single source version from pyproject.toml try: __version__ = importlib.metadata.version(__package__.split("_", maxsplit=1)[0]) except importlib.metadata.PackageNotFoundError: __version__ = "0.0.0" ================================================ FILE: src/electionguard/ballot.py ================================================ from dataclasses import dataclass, field, replace from datetime import datetime, timezone from enum import Enum from functools import cached_property, reduce from typing import ( Any, Dict, List, Iterable, Optional, Protocol, runtime_checkable, ) from .ballot_code import get_ballot_code from .chaum_pedersen import ( ConstantChaumPedersenProof, DisjunctiveChaumPedersenProof, make_constant_chaum_pedersen, make_disjunctive_chaum_pedersen, ) from .election_object_base import ( ElectionObjectBase, OrderedObjectBase, sequence_order_sort, list_eq, ) from .elgamal import ( ElGamalCiphertext, ElGamalPublicKey, HashedElGamalCiphertext, elgamal_add, ) from .group import add_q, ElementModQ, ZERO_MOD_Q from .hash import CryptoHashCheckable, hash_elems from .logs import log_warning from .manifest import ContestDescription from .type import SelectionId from .utils import ( ContestException, NullVoteException, OverVoteException, UnderVoteException, flatmap_optional, to_ticks, ) @dataclass(unsafe_hash=True) class PlaintextBallotSelection(ElectionObjectBase): """ A BallotSelection represents an individual selection on a ballot. This class accepts a `vote` integer field which has no constraints in the ElectionGuard Data Specification, but is constrained logically in the application to resolve to `False` or `True` aka only 0 and 1 is supported for now. This class can also be designated as `is_placeholder_selection` which has no context to the data specification but is useful for running validity checks internally Write_in field exists to support the cleartext representation of a write-in candidate value. """ vote: int is_placeholder_selection: bool = field(default=False) """Determines if this is a placeholder selection""" write_in: Optional[str] = field(default=None) """ Write_in field exists to support the cleartext representation of a write-in candidate value. """ def is_valid(self, expected_object_id: str) -> bool: """ Given a PlaintextBallotSelection validates that the object matches an expected object and that the plaintext string can resolve to a valid representation """ if self.object_id != expected_object_id: log_warning( f"invalid object_id: expected({expected_object_id}) actual({self.object_id})" ) return False vote = self.vote if vote < 0 or vote > 1: log_warning(f"Currently only supporting choices of 0 or 1: {str(self)}") return False return True def __eq__(self, other: Any) -> bool: return ( isinstance(other, PlaintextBallotSelection) and self.object_id == other.object_id and self.vote == other.vote and self.is_placeholder_selection == other.is_placeholder_selection and self.write_in == other.write_in ) def __ne__(self, other: Any) -> bool: return not self.__eq__(other) @runtime_checkable class CiphertextSelection(Protocol): """ Encrypted selection """ object_id: str sequence_order: int """Order the selection.""" description_hash: ElementModQ """The SelectionDescription hash""" ciphertext: ElGamalCiphertext """The encrypted representation of the selection""" @dataclass(eq=True, unsafe_hash=True) class CiphertextBallotSelection( OrderedObjectBase, CiphertextSelection, CryptoHashCheckable ): """ A CiphertextBallotSelection represents an individual encrypted selection on a ballot. This class accepts a `description_hash` and a `ciphertext` as required parameters in its constructor. When a selection is encrypted, the `description_hash` and `ciphertext` required fields must be populated at construction however the `nonce` is also usually provided by convention. After construction, the `crypto_hash` field is populated automatically in the `__post_init__` cycle A consumer of this object has the option to discard the `nonce` and/or discard the `proof`, or keep both values. By discarding the `nonce`, the encrypted representation and `proof` can only be regenerated if the nonce was derived from the ballot's master nonce. If the nonce used for this selection is truly random, and it is discarded, then the proofs cannot be regenerated. By keeping the `nonce`, or deriving the selection nonce from the ballot nonce, an external system can regenerate the proofs on demand. This is useful for storage or memory constrained systems. By keeping the `proof` the nonce is not required fotor verify the encrypted selection. """ description_hash: ElementModQ """The SelectionDescription hash""" ciphertext: ElGamalCiphertext """The encrypted representation of the vote field""" crypto_hash: ElementModQ """The hash of the encrypted values""" is_placeholder_selection: bool = field(default=False) """Determines if this is a placeholder selection""" nonce: Optional[ElementModQ] = field(default=None) """The nonce used to generate the encryption. Sensitive & should be treated as a secret""" proof: Optional[DisjunctiveChaumPedersenProof] = field(default=None) """The proof that demonstrates the selection is an encryption of 0 or 1, and was encrypted using the `nonce`""" def is_valid_encryption( self, encryption_seed: ElementModQ, elgamal_public_key: ElGamalPublicKey, crypto_extended_base_hash: ElementModQ, ) -> bool: """ Given an encrypted BallotSelection, validates the encryption state against a specific seed and public key. Calling this function expects that the object is in a well-formed encrypted state with the elgamal encrypted `message` field populated along with the DisjunctiveChaumPedersenProof`proof` populated. the ElementModQ `description_hash` and the ElementModQ `crypto_hash` are also checked. :param encryption_seed: the hash of the SelectionDescription, or whatever `ElementModQ` was used to populate the `description_hash` field. :param elgamal_public_key: The election public key """ if encryption_seed != self.description_hash: log_warning( ( f"mismatching selection hash: {self.object_id} expected({str(encryption_seed)}), " f"actual({str(self.description_hash)})" ) ) return False recalculated_crypto_hash = self.crypto_hash_with(encryption_seed) if self.crypto_hash != recalculated_crypto_hash: log_warning( ( f"mismatching crypto hash: {self.object_id} expected({str(recalculated_crypto_hash)}), " f"actual({str(self.crypto_hash)})" ) ) return False if self.proof is None: log_warning(f"no proof exists for: {self.object_id}") return False return self.proof.is_valid( self.ciphertext, elgamal_public_key, crypto_extended_base_hash ) def crypto_hash_with(self, encryption_seed: ElementModQ) -> ElementModQ: """ Given an encrypted BallotSelection, generates a hash, suitable for rolling up into a hash for an entire ballot / ballot code. Of note, this particular hash examines the `encryption_seed` and `message`, but not the proof. This is deliberate, allowing for the possibility of ElectionGuard variants running on much more limited hardware, wherein the Disjunctive Chaum-Pedersen proofs might be computed later on. In most cases the encryption_seed should match the `description_hash` """ return _ciphertext_ballot_selection_crypto_hash_with( self.object_id, encryption_seed, self.ciphertext ) def _ciphertext_ballot_selection_crypto_hash_with( object_id: str, encryption_seed: ElementModQ, ciphertext: ElGamalCiphertext ) -> ElementModQ: return hash_elems(object_id, encryption_seed, ciphertext.crypto_hash()) def make_ciphertext_ballot_selection( object_id: str, sequence_order: int, description_hash: ElementModQ, ciphertext: ElGamalCiphertext, elgamal_public_key: ElGamalPublicKey, crypto_extended_base_hash: ElementModQ, proof_seed: ElementModQ, selection_representation: int, is_placeholder_selection: bool = False, nonce: Optional[ElementModQ] = None, crypto_hash: Optional[ElementModQ] = None, proof: Optional[DisjunctiveChaumPedersenProof] = None, ) -> CiphertextBallotSelection: """ Constructs a `CipherTextBallotSelection` object. Most of the parameters here match up to fields in the class, but this helper function will optionally compute a Chaum-Pedersen proof if the given nonce isn't `None`. Likewise, if a crypto_hash is not provided, it will be derived from the other fields. """ if crypto_hash is None: crypto_hash = _ciphertext_ballot_selection_crypto_hash_with( object_id, description_hash, ciphertext ) if proof is None: proof = flatmap_optional( nonce, lambda n: make_disjunctive_chaum_pedersen( ciphertext, n, elgamal_public_key, crypto_extended_base_hash, proof_seed, selection_representation, ), ) return CiphertextBallotSelection( object_id, sequence_order, description_hash, ciphertext, crypto_hash, is_placeholder_selection, nonce, proof, ) @dataclass(unsafe_hash=True) class PlaintextBallotContest(ElectionObjectBase): """ A PlaintextBallotContest represents the selections made by a voter for a specific ContestDescription this class can be either a partial or a complete representation of a contest dataset. Specifically, a partial representation must include at a minimum the "affirmative" selections of a contest. A complete representation of a ballot must include both affirmative and negative selections of the contest, AND the placeholder selections necessary to satisfy the ConstantChaumPedersen proof in the CiphertextBallotContest. Typically partial contests are passed into Electionguard for memory constrained systems, while complete contests are passed into ElectionGuard when running encryption on an existing dataset. """ ballot_selections: List[PlaintextBallotSelection] = field( default_factory=lambda: [] ) """Collection of ballot selections""" @cached_property def selected_ids(self) -> List[SelectionId]: return [ selection.object_id for selection in self.ballot_selections if selection.vote > 0 ] @cached_property def total_selected(self) -> int: """Returns the total number of selected selections.""" return reduce( lambda prev, next: prev + (1 if next.vote > 0 else 0), self.ballot_selections, 0, ) @cached_property def total_votes(self) -> int: """Returns the total number of votes on selections.""" return reduce(lambda prev, next: prev + next.vote, self.ballot_selections, 0) @cached_property def write_ins(self) -> Optional[Dict[SelectionId, str]]: write_ins = { selection.object_id: selection.write_in for selection in self.ballot_selections if selection.write_in is not None # Required due to empty strings } return write_ins if len(write_ins) else None def valid(self, description: ContestDescription) -> None: """Determine if a contest is valid.""" # Contest id matches description and ballot selections don't exceed description if ( self.object_id != description.object_id or len(self.ballot_selections) > len(description.ballot_selections) or not description.is_valid() ): raise ContestException( self.object_id, override_message=f"invalid format of contest or description for contest {self.object_id}", ) # Selections ids match description selection_ids = { selection.object_id for selection in description.ballot_selections } for selection in self.ballot_selections: if selection.object_id not in selection_ids: raise ContestException( self.object_id, override_message=f"invalid selection id ${selection.object_id} on contest {self.object_id}", ) # Specialty cases if self.total_selected < 1: raise NullVoteException(self.object_id) if self.total_selected < description.number_elected: raise UnderVoteException(self.object_id) if self.total_selected > description.number_elected: raise OverVoteException(self.object_id, self.selected_ids) if description.votes_allowed is not None: if self.total_votes > description.votes_allowed: raise OverVoteException(self.object_id, self.selected_ids) # Support for other cases such as cumulative voting not currently supported. # (individual selections being an encryption of > 1) if self.total_selected < description.votes_allowed: raise ContestException( self.object_id, override_message=f"`on contest {self.object_id}: only n-of-m style elections are supported", ) def is_valid( self, expected_object_id: str, expected_number_selections: int, expected_number_elected: int, votes_allowed: Optional[int] = None, ) -> bool: """ Given a PlaintextBallotContest returns true if the state is representative of the expected values. Note: because this class supports partial representations, undervotes are considered a valid state. """ if self.object_id != expected_object_id: log_warning( ( f"invalid object_id: expected({expected_object_id}) " f"actual({self.object_id})" ) ) return False if len(self.ballot_selections) > expected_number_selections: log_warning( ( f"invalid number_selections: expected({expected_number_selections}) " f"actual({len(self.ballot_selections)})" ) ) return False number_elected = 0 votes = 0 # Verify the selections are well-formed for selection in self.ballot_selections: votes += selection.vote if selection.vote >= 1: number_elected += 1 if number_elected > expected_number_elected: log_warning( f"invalid number_elected: expected({expected_number_elected}) actual({number_elected})" ) return False if votes_allowed is not None and votes > votes_allowed: log_warning(f"invalid votes: expected({votes_allowed}) actual({votes})") return False return True def __eq__(self, other: Any) -> bool: return isinstance(other, PlaintextBallotContest) and list_eq( self.ballot_selections, other.ballot_selections ) def __ne__(self, other: Any) -> bool: return not self.__eq__(other) @dataclass class CiphertextContest(OrderedObjectBase): """ Base encrypted contest for both tally and ballot """ description_hash: ElementModQ """The description hash""" selections: Iterable[CiphertextSelection] """Collection of selections""" @dataclass(unsafe_hash=True) class CiphertextBallotContest(OrderedObjectBase, CryptoHashCheckable): """ A CiphertextBallotContest represents the selections made by a voter for a specific ContestDescription CiphertextBallotContest can only be a complete representation of a contest dataset. While PlaintextBallotContest supports a partial representation, a CiphertextBallotContest includes all data necessary for a verifier to verify the contest. Specifically, it includes both explicit affirmative and negative selections of the contest, as well as the placeholder selections that satisfy the ConstantChaumPedersen proof. Similar to `CiphertextBallotSelection` the consuming application can choose to discard or keep both the `nonce` and the `proof` in some circumstances. For deterministic nonce's derived from the master nonce, both values can be regenerated. If the `nonce` for this contest is completely random, then it is required in order to regenerate the proof. """ description_hash: ElementModQ """Hash from contestDescription""" ballot_selections: List[CiphertextBallotSelection] """Collection of ballot selections""" ciphertext_accumulation: ElGamalCiphertext """The encrypted representation of all of the vote fields (the contest total)""" crypto_hash: ElementModQ """Hash of the encrypted values""" nonce: Optional[ElementModQ] = None """The nonce used to generate the encryption. Sensitive & should be treated as a secret""" proof: Optional[ConstantChaumPedersenProof] = None """ The proof demonstrates the sum of the selections does not exceed the maximum available selections for the contest, and that the proof was generated with the nonce """ extended_data: Optional[HashedElGamalCiphertext] = field(default=None) """encrypted representation of the extended_data field""" def __eq__(self, other: Any) -> bool: return ( isinstance(other, CiphertextBallotContest) and self.object_id == other.object_id and list_eq(self.ballot_selections, other.ballot_selections) and self.description_hash == other.description_hash and self.crypto_hash == other.crypto_hash and self.nonce == other.nonce and self.proof == other.proof ) def __ne__(self, other: Any) -> bool: return not self.__eq__(other) def aggregate_nonce(self) -> Optional[ElementModQ]: """ :return: an aggregate nonce for the contest composed of the nonces of the selections """ return _ciphertext_ballot_contest_aggregate_nonce( self.object_id, self.ballot_selections ) def crypto_hash_with(self, encryption_seed: ElementModQ) -> ElementModQ: """ Given an encrypted BallotContest, generates a hash, suitable for rolling up into a hash for an entire ballot / ballot code. Of note, this particular hash examines the `encryption_seed` and `ballot_selections`, but not the proof. This is deliberate, allowing for the possibility of ElectionGuard variants running on much more limited hardware, wherein the Disjunctive Chaum-Pedersen proofs might be computed later on. In most cases, the encryption_seed is the description_hash """ return _ciphertext_ballot_context_crypto_hash( self.object_id, self.ballot_selections, encryption_seed ) def elgamal_accumulate(self) -> ElGamalCiphertext: """ Add the individual ballot_selections `message` fields together, suitable for use in a Chaum-Pedersen proof. """ return _ciphertext_ballot_elgamal_accumulate(self.ballot_selections) def is_valid_encryption( self, encryption_seed: ElementModQ, elgamal_public_key: ElGamalPublicKey, crypto_extended_base_hash: ElementModQ, ) -> bool: """ Given an encrypted BallotContest, validates the encryption state against a specific seed and public key by verifying the accumulated sum of selections match the proof. Calling this function expects that the object is in a well-formed encrypted state with the `ballot_selections` populated with valid encrypted ballot selections, the ElementModQ `description_hash`, the ElementModQ `crypto_hash`, and the ConstantChaumPedersenProof all populated. Specifically, the seed in this context is the hash of the ContestDescription, or whatever `ElementModQ` was used to populate the `description_hash` field. """ if encryption_seed != self.description_hash: log_warning( ( f"mismatching contest hash: {self.object_id} expected({str(encryption_seed)}), " f"actual({str(self.description_hash)})" ) ) return False recalculated_crypto_hash = self.crypto_hash_with(encryption_seed) if self.crypto_hash != recalculated_crypto_hash: log_warning( ( f"mismatching crypto hash: {self.object_id} expected({str(recalculated_crypto_hash)}), " f"actual({str(self.crypto_hash)})" ) ) return False # NOTE: this check does not verify the proofs of the individual selections by design. if self.proof is None: log_warning(f"no proof exists for: {self.object_id}") return False computed_ciphertext_accumulation = self.elgamal_accumulate() # Verify that the contest ciphertext matches the elgamal accumulation of all selections if self.ciphertext_accumulation != computed_ciphertext_accumulation: log_warning( f"ciphertext does not equal elgamal accumulation for : {self.object_id}" ) return False # Verify the sum of the selections matches the proof return self.proof.is_valid( computed_ciphertext_accumulation, elgamal_public_key, crypto_extended_base_hash, ) def _ciphertext_ballot_elgamal_accumulate( ballot_selections: List[CiphertextBallotSelection], ) -> ElGamalCiphertext: return elgamal_add(*[selection.ciphertext for selection in ballot_selections]) def _ciphertext_ballot_context_crypto_hash( object_id: str, ballot_selections: List[CiphertextBallotSelection], encryption_seed: ElementModQ, ) -> ElementModQ: if len(ballot_selections) == 0: log_warning( f"mismatching ballot_selections state: {object_id} expected(some), actual(none)" ) return ZERO_MOD_Q selection_hashes = [ selection.crypto_hash for selection in sequence_order_sort(ballot_selections) ] return hash_elems(object_id, encryption_seed, *selection_hashes) def _ciphertext_ballot_contest_aggregate_nonce( object_id: str, ballot_selections: List[CiphertextBallotSelection] ) -> Optional[ElementModQ]: selection_nonces: List[ElementModQ] = [] for selection in ballot_selections: if selection.nonce is None: log_warning( f"missing nonce values for contest {object_id} cannot calculate aggregate nonce" ) return None selection_nonces.append(selection.nonce) return add_q(*selection_nonces) def make_ciphertext_ballot_contest( object_id: str, sequence_order: int, description_hash: ElementModQ, ballot_selections: List[CiphertextBallotSelection], elgamal_public_key: ElGamalPublicKey, crypto_extended_base_hash: ElementModQ, proof_seed: ElementModQ, number_elected: int, crypto_hash: Optional[ElementModQ] = None, proof: Optional[ConstantChaumPedersenProof] = None, nonce: Optional[ElementModQ] = None, extended_data: Optional[HashedElGamalCiphertext] = None, ) -> CiphertextBallotContest: """ Constructs a `CipherTextBallotContest` object. Most of the parameters here match up to fields in the class, but this helper function will optionally compute a Chaum-Pedersen proof if the ballot selections include their encryption nonces. Likewise, if a crypto_hash is not provided, it will be derived from the other fields. """ if crypto_hash is None: crypto_hash = _ciphertext_ballot_context_crypto_hash( object_id, ballot_selections, description_hash ) aggregate = _ciphertext_ballot_contest_aggregate_nonce(object_id, ballot_selections) elgamal_accumulation = _ciphertext_ballot_elgamal_accumulate(ballot_selections) if proof is None: proof = flatmap_optional( aggregate, lambda ag: make_constant_chaum_pedersen( elgamal_accumulation, number_elected, ag, elgamal_public_key, proof_seed, crypto_extended_base_hash, ), ) return CiphertextBallotContest( object_id, sequence_order, description_hash, ballot_selections, elgamal_accumulation, crypto_hash, nonce, proof, extended_data, ) @dataclass(unsafe_hash=True) class PlaintextBallot(ElectionObjectBase): """ A PlaintextBallot represents a voters selections for a given ballot and ballot style :field object_id: A unique Ballot ID that is relevant to the external system """ style_id: str """The `object_id` of the `BallotStyle` in the `Election` Manifest""" contests: List[PlaintextBallotContest] """The list of contests for this ballot""" def is_valid(self, expected_ballot_style_id: str) -> bool: """ Check if expected ballot style is valid :param expected_ballot_style_id: Expected ballot style id :return: True if valid """ if self.style_id != expected_ballot_style_id: log_warning( ( f"invalid ballot_style: for: {self.object_id} expected({expected_ballot_style_id}) " f"actual({self.style_id})" ) ) return False return True def __eq__(self, other: Any) -> bool: return ( isinstance(other, PlaintextBallot) and self.style_id == other.style_id and list_eq(self.contests, other.contests) ) def __ne__(self, other: Any) -> bool: return not self.__eq__(other) # pylint: disable=too-many-instance-attributes @dataclass(unsafe_hash=True) class CiphertextBallot(ElectionObjectBase, CryptoHashCheckable): """ A CiphertextBallot represents a voters encrypted selections for a given ballot and ballot style. When a ballot is in it's complete, encrypted state, the `nonce` is the master nonce from which all other nonces can be derived to encrypt the ballot. Allong with the `nonce` fields on `Ballotcontest` and `BallotSelection`, this value is sensitive. Don't make this directly. Use `make_ciphertext_ballot` instead. :field object_id: A unique Ballot ID that is relevant to the external system """ style_id: str """The `object_id` of the `BallotStyle` in the `Election` Manifest""" manifest_hash: ElementModQ """Hash of the election manifest""" code_seed: ElementModQ """Seed for ballot code""" contests: List[CiphertextBallotContest] """List of contests for this ballot""" code: ElementModQ """Unique ballot code for this ballot""" timestamp: int """Timestamp at which the ballot encryption is generated in tick""" crypto_hash: ElementModQ """The hash of the encrypted ballot representation""" nonce: Optional[ElementModQ] """The nonce used to encrypt this ballot. Sensitive & should be treated as a secret""" def __eq__(self, other: Any) -> bool: return ( isinstance(other, CiphertextBallot) and self.object_id == other.object_id and self.style_id == other.style_id and self.manifest_hash == other.manifest_hash and self.code_seed == other.code_seed and list_eq(self.contests, other.contests) and self.code == other.code and self.timestamp == other.timestamp and self.crypto_hash == other.crypto_hash and self.nonce == other.nonce ) def __ne__(self, other: Any) -> bool: return not self.__eq__(other) @staticmethod def nonce_seed( manifest_hash: ElementModQ, object_id: str, nonce: ElementModQ ) -> ElementModQ: """ :return: a representation of the election and the external Id in the nonce's used to derive other nonce values on the ballot """ return hash_elems(manifest_hash, object_id, nonce) def hashed_ballot_nonce(self) -> Optional[ElementModQ]: """ :return: a hash value derived from the description hash, the object id, and the nonce value suitable for deriving other nonce values on the ballot """ if self.nonce is None: log_warning( f"missing nonce for ballot {self.object_id} could not derive from null nonce" ) return None return self.nonce_seed(self.manifest_hash, self.object_id, self.nonce) def crypto_hash_with(self, encryption_seed: ElementModQ) -> ElementModQ: """ Given an encrypted Ballot, generates a hash, suitable for rolling up into a hash for an entire ballot / ballot code. Of note, this particular hash examines the `manifest_hash` and `ballot_selections`, but not the proof. This is deliberate, allowing for the possibility of ElectionGuard variants running on much more limited hardware, wherein the Disjunctive Chaum-Pedersen proofs might be computed later on. """ if len(self.contests) == 0: log_warning( f"mismatching contests state: {self.object_id} expected(some), actual(none)" ) return ZERO_MOD_Q contest_hashes = [contest.crypto_hash for contest in self.contests] return hash_elems(self.object_id, encryption_seed, *contest_hashes) def is_valid_encryption( self, encryption_seed: ElementModQ, elgamal_public_key: ElGamalPublicKey, crypto_extended_base_hash: ElementModQ, ) -> bool: """ Given an encrypted Ballot, validates the encryption state against a specific seed and public key by verifying the states of this ballot's children (BallotContest's and BallotSelection's). Calling this function expects that the object is in a well-formed encrypted state with the `contests` populated with valid encrypted ballot selections, and the ElementModQ `manifest_hash` also populated. Specifically, the seed in this context is the hash of the Election Manifest, or whatever `ElementModQ` was used to populate the `manifest_hash` field. """ if encryption_seed != self.manifest_hash: log_warning( ( f"mismatching ballot hash: {self.object_id} expected({str(encryption_seed)}), " f"actual({str(self.manifest_hash)})" ) ) return False recalculated_crypto_hash = self.crypto_hash_with(encryption_seed) if self.crypto_hash != recalculated_crypto_hash: log_warning( ( f"mismatching crypto hash: {self.object_id} expected({str(recalculated_crypto_hash)}), " f"actual({str(self.crypto_hash)})" ) ) return False # Check the proofs on the ballot valid_proofs: List[bool] = [] for contest in self.contests: for selection in contest.ballot_selections: valid_proofs.append( selection.is_valid_encryption( selection.description_hash, elgamal_public_key, crypto_extended_base_hash, ) ) valid_proofs.append( contest.is_valid_encryption( contest.description_hash, elgamal_public_key, crypto_extended_base_hash, ) ) return all(valid_proofs) class BallotBoxState(Enum): """ Enumeration used when marking a ballot as cast or spoiled """ CAST = 1 """ A ballot that has been explicitly cast """ SPOILED = 2 """ A ballot that has been explicitly spoiled """ UNKNOWN = 999 """ A ballot whose state is unknown to ElectionGuard and will not be included in any election results """ @dataclass(unsafe_hash=True) class SubmittedBallot(CiphertextBallot): """ A `SubmittedBallot` represents a ballot that is submitted for inclusion in election results. A submitted ballot is or is about to be either cast or spoiled. The state supports the `BallotBoxState.UNKNOWN` enumeration to indicate that this object is mutable and has not yet been explicitly assigned a specific state. Note, additionally, this ballot includes all proofs but no nonces. Do not make this class directly. Use `make_ciphertext_submitted_ballot` instead. """ state: BallotBoxState def __eq__(self, other: Any) -> bool: return ( isinstance(other, SubmittedBallot) and super().__eq__(other) and self.state == other.state ) def __ne__(self, other: Any) -> bool: return not self.__eq__(other) def make_ciphertext_ballot( object_id: str, style_id: str, manifest_hash: ElementModQ, code_seed: Optional[ElementModQ], contests: List[CiphertextBallotContest], nonce: Optional[ElementModQ] = None, timestamp: Optional[int] = None, ballot_code: Optional[ElementModQ] = None, ) -> CiphertextBallot: """ Makes a `CiphertextBallot`, initially in the state where it's neither been cast nor spoiled. :param object_id: the object_id of this specific ballot :param style_id: The `object_id` of the `BallotStyle` in the `Election` Manifest :param manifest_hash: Hash of the election manifest :param crypto_base_hash: Hash of the cryptographic election context :param contests: List of contests for this ballot :param timestamp: Timestamp at which the ballot encryption is generated in tick :param code_seed: Seed for ballot code :param nonce: optional nonce used as part of the encryption process """ if len(contests) == 0: log_warning("ciphertext ballot with no contests") contest_hash = create_ballot_hash(object_id, manifest_hash, contests) timestamp = to_ticks(datetime.now()) if timestamp is None else timestamp if code_seed is None: code_seed = manifest_hash if ballot_code is None: ballot_code = get_ballot_code(code_seed, timestamp, contest_hash) return CiphertextBallot( object_id, style_id, manifest_hash, code_seed, contests, ballot_code, timestamp, contest_hash, nonce, ) def create_ballot_hash( ballot_id: str, description_hash: ElementModQ, contests: List[CiphertextBallotContest], ) -> ElementModQ: """Create the hash of the ballot contests""" contest_hashes = [contest.crypto_hash for contest in sequence_order_sort(contests)] return hash_elems(ballot_id, description_hash, *contest_hashes) def make_ciphertext_submitted_ballot( object_id: str, style_id: str, manifest_hash: ElementModQ, code_seed: Optional[ElementModQ], contests: List[CiphertextBallotContest], ballot_code: Optional[ElementModQ], timestamp: Optional[int] = None, state: BallotBoxState = BallotBoxState.UNKNOWN, ) -> SubmittedBallot: """ Makes a `SubmittedBallot`, ensuring that no nonces are part of the contests. :param object_id: the object_id of this specific ballot :param style_id: The `object_id` of the `BallotStyle` in the `Election` Manifest :param manifest_hash: Hash of the election manifest :param code_seed: Seed for ballot code :param contests: List of contests for this ballot :param timestamp: Timestamp at which the ballot encryption is generated in tick :param state: ballot box state """ if len(contests) == 0: log_warning("ciphertext ballot with no contests") contest_hashes = [contest.crypto_hash for contest in sequence_order_sort(contests)] contest_hash = hash_elems(object_id, manifest_hash, *contest_hashes) timestamp = to_ticks(datetime.now(timezone.utc)) if timestamp is None else timestamp if code_seed is None: code_seed = manifest_hash if ballot_code is None: ballot_code = get_ballot_code(code_seed, timestamp, contest_hash) # copy the contests and selections, removing all nonces new_contests: List[CiphertextBallotContest] = [] for contest in contests: new_selections = [ replace(selection, nonce=None) for selection in contest.ballot_selections ] new_contest = replace(contest, nonce=None, ballot_selections=new_selections) new_contests.append(new_contest) return SubmittedBallot( object_id, style_id, manifest_hash, code_seed, new_contests, ballot_code, timestamp, contest_hash, None, state, ) ================================================ FILE: src/electionguard/ballot_box.py ================================================ from dataclasses import dataclass, field from typing import Dict, Optional from .ballot import ( BallotBoxState, CiphertextBallot, SubmittedBallot, make_ciphertext_submitted_ballot, ) from .ballot_validator import ballot_is_valid_for_election from .data_store import DataStore from .election import CiphertextElectionContext from .logs import log_warning from .manifest import InternalManifest from .type import BallotId @dataclass class BallotBox: """A stateful convenience wrapper to cache election data.""" _internal_manifest: InternalManifest = field() _encryption: CiphertextElectionContext = field() _store: DataStore = field(default_factory=lambda: DataStore()) def cast(self, ballot: CiphertextBallot) -> Optional[SubmittedBallot]: """Cast a specific encrypted `CiphertextBallot`.""" return submit_ballot_to_box( ballot, BallotBoxState.CAST, self._internal_manifest, self._encryption, self._store, ) def spoil(self, ballot: CiphertextBallot) -> Optional[SubmittedBallot]: """Spoil a specific encrypted `CiphertextBallot`.""" return submit_ballot_to_box( ballot, BallotBoxState.SPOILED, self._internal_manifest, self._encryption, self._store, ) def submit_ballot_to_box( ballot: CiphertextBallot, state: BallotBoxState, internal_manifest: InternalManifest, context: CiphertextElectionContext, store: DataStore, ) -> Optional[SubmittedBallot]: """ Submit a ballot within the context of a specified election and against an existing data store Verified that the ballot is valid for the election `internal_manifest` and `context` and that the ballot has not already been cast or spoiled. :return: a `SubmittedBallot` or `None` if there was an error """ if not ballot_is_valid_for_election(ballot, internal_manifest, context, True): log_warning(f"ballot: {ballot.object_id} failed validity check") return None existing_ballot = store.get(ballot.object_id) if existing_ballot is not None: log_warning( f"error accepting ballot, {ballot.object_id} already exists with state: {existing_ballot.state}" ) return None # TODO: ISSUE #56: check if the ballot includes the nonce, and regenerate the proofs # TODO: ISSUE #56: check if the ballot includes the proofs, if it does not include the nonce ballot_box_ballot = submit_ballot(ballot, state) store.set(ballot_box_ballot.object_id, ballot_box_ballot) return store.get(ballot_box_ballot.object_id) def get_ballots( store: DataStore, state: Optional[BallotBoxState] ) -> Dict[BallotId, SubmittedBallot]: """Get ballots from the store optionally filtering on state.""" return { ballot_id: ballot for (ballot_id, ballot) in store.items() if state is None or ballot.state == state } def submit_ballot( ballot: CiphertextBallot, state: BallotBoxState = BallotBoxState.UNKNOWN ) -> SubmittedBallot: """ Convert a `CiphertextBallot` into a `SubmittedBallot`, with all nonces removed. """ return make_ciphertext_submitted_ballot( ballot.object_id, ballot.style_id, ballot.manifest_hash, ballot.code_seed, ballot.contests, ballot.code, ballot.timestamp, state, ) def cast_ballot(ballot: CiphertextBallot) -> SubmittedBallot: """ Convert a `CiphertextBallot` into a `SubmittedBallot`, with all nonces removed. Declare a ballot as CAST. """ return submit_ballot( ballot, BallotBoxState.CAST, ) def spoil_ballot(ballot: CiphertextBallot) -> SubmittedBallot: """ Convert a `CiphertextBallot` into a `SubmittedBallot`, with all nonces removed. Declare a ballot as CAST. """ return submit_ballot( ballot, BallotBoxState.SPOILED, ) ================================================ FILE: src/electionguard/ballot_code.py ================================================ from .hash import hash_elems from .group import ElementModQ def get_hash_for_device( device_id: int, session_id: int, launch_code: int, location: str ) -> ElementModQ: """ Get starting hash for given device. :param device_id: Unique identifier of device :param session_id: Unique identifier for the session :param launch_code: A unique launch code for the election :param location: Location of device :return: Starting hash of device """ return hash_elems(device_id, session_id, launch_code, location) def get_ballot_code( prev_code: ElementModQ, timestamp: int, ballot_hash: ElementModQ ) -> ElementModQ: """ Get the rotated code for a particular ballot. :param prev_code: Previous code or starting hash from device :param timestamp: Timestamp in ticks :param ballot_hash: Hash of ballot :return: code """ return hash_elems(prev_code, timestamp, ballot_hash) ================================================ FILE: src/electionguard/ballot_compact.py ================================================ from dataclasses import dataclass from typing import Dict, List from .ballot import ( CiphertextBallot, SubmittedBallot, PlaintextBallot, PlaintextBallotContest, PlaintextBallotSelection, make_ciphertext_submitted_ballot, ) from .ballot_box import BallotBoxState from .election import CiphertextElectionContext from .election_object_base import sequence_order_sort from .encrypt import encrypt_ballot_contests from .group import ElementModQ from .manifest import ( ContestDescriptionWithPlaceholders, InternalManifest, ) from .utils import get_optional YES_VOTE = 1 NO_VOTE = 0 @dataclass class CompactPlaintextBallot: """A compact plaintext representation of ballot minimized for data size""" object_id: str style_id: str selections: List[bool] write_ins: Dict[int, str] @dataclass class CompactSubmittedBallot: """A compact submitted ballot minimized for data size""" compact_plaintext_ballot: CompactPlaintextBallot timestamp: int ballot_nonce: ElementModQ code_seed: ElementModQ code: ElementModQ ballot_box_state: BallotBoxState def compress_plaintext_ballot(ballot: PlaintextBallot) -> CompactPlaintextBallot: """Compress a plaintext ballot into a compact plaintext ballot""" selections = _get_compact_selections(ballot) extended_data = _get_compact_write_ins(ballot) return CompactPlaintextBallot( ballot.object_id, ballot.style_id, selections, extended_data ) def compress_submitted_ballot( ballot: SubmittedBallot, plaintext_ballot: PlaintextBallot, ballot_nonce: ElementModQ, ) -> CompactSubmittedBallot: """Compress a submitted ballot into a compact submitted ballot""" return CompactSubmittedBallot( compress_plaintext_ballot(plaintext_ballot), ballot.timestamp, ballot_nonce, ballot.code_seed, ballot.code, ballot.state, ) def expand_compact_submitted_ballot( compact_ballot: CompactSubmittedBallot, internal_manifest: InternalManifest, context: CiphertextElectionContext, ) -> SubmittedBallot: """ Expand a compact submitted ballot using context and the election manifest into a submitted ballot """ # Expand ballot and encrypt & hash contests plaintext_ballot = expand_compact_plaintext_ballot( compact_ballot.compact_plaintext_ballot, internal_manifest ) nonce_seed = CiphertextBallot.nonce_seed( internal_manifest.manifest_hash, compact_ballot.compact_plaintext_ballot.object_id, compact_ballot.ballot_nonce, ) contests = get_optional( encrypt_ballot_contests( plaintext_ballot, internal_manifest, context, nonce_seed ) ) return make_ciphertext_submitted_ballot( plaintext_ballot.object_id, plaintext_ballot.style_id, internal_manifest.manifest_hash, compact_ballot.code_seed, contests, compact_ballot.code, compact_ballot.timestamp, compact_ballot.ballot_box_state, ) def expand_compact_plaintext_ballot( compact_ballot: CompactPlaintextBallot, internal_manifest: InternalManifest ) -> PlaintextBallot: """Expand a compact plaintext ballot into the original plaintext ballot""" return PlaintextBallot( compact_ballot.object_id, compact_ballot.style_id, _get_plaintext_contests(compact_ballot, internal_manifest), ) def _get_compact_selections(ballot: PlaintextBallot) -> List[bool]: selections = [] for contest in ballot.contests: for selection in contest.ballot_selections: selections.append(selection.vote == YES_VOTE) return selections def _get_compact_write_ins(ballot: PlaintextBallot) -> Dict[int, str]: write_ins = {} index = 0 for contest in ballot.contests: for selection in contest.ballot_selections: index += 1 if selection.write_in: write_ins[index] = selection.write_in return write_ins def _get_plaintext_contests( compact_ballot: CompactPlaintextBallot, internal_manifest: InternalManifest ) -> List[PlaintextBallotContest]: """Get ballot contests from compact plaintext ballot""" index = 0 ballot_style_contests = _get_ballot_style_contests( compact_ballot.style_id, internal_manifest ) contests: List[PlaintextBallotContest] = [] for manifest_contest in sequence_order_sort(internal_manifest.contests): contest_in_style = ( ballot_style_contests.get(manifest_contest.object_id) is not None ) # Iterate through selections. If contest not in style, mark placeholder selections: List[PlaintextBallotSelection] = [] for selection in sequence_order_sort(manifest_contest.ballot_selections): selections.append( PlaintextBallotSelection( selection.object_id, YES_VOTE if compact_ballot.selections[index] else NO_VOTE, not contest_in_style, compact_ballot.write_ins.get(index), ) ) index += 1 contests.append(PlaintextBallotContest(manifest_contest.object_id, selections)) return contests def _get_ballot_style_contests( ballot_style_id: str, internal_manifest: InternalManifest ) -> Dict[str, ContestDescriptionWithPlaceholders]: ballot_style_contests = internal_manifest.get_contests_for(ballot_style_id) return {contest.object_id: contest for contest in ballot_style_contests} ================================================ FILE: src/electionguard/ballot_validator.py ================================================ from .ballot import CiphertextBallot, CiphertextBallotContest, CiphertextBallotSelection from .election import CiphertextElectionContext from .logs import log_warning from .manifest import ( ContestDescriptionWithPlaceholders, InternalManifest, SelectionDescription, ) def ballot_is_valid_for_election( ballot: CiphertextBallot, internal_manifest: InternalManifest, context: CiphertextElectionContext, should_validate: bool, ) -> bool: """ Determine if a ballot is valid for a given election """ if not ballot_is_valid_for_style(ballot, internal_manifest): return False if should_validate: if not ballot.is_valid_encryption( internal_manifest.manifest_hash, context.elgamal_public_key, context.crypto_extended_base_hash, ): log_warning( f"ballot_is_valid_for_election: mismatching ballot encryption {ballot.object_id}" ) return False return True def selection_is_valid_for_style( selection: CiphertextBallotSelection, description: SelectionDescription ) -> bool: """ Determine if selection is valid for ballot style :param selection: Ballot selection :param description: Selection description :return: Is valid """ if selection.description_hash != description.crypto_hash(): log_warning( ( f"ballot is not valid for style: mismatched selection description hash {selection.description_hash} " f"for selection {description.object_id} hash {description.crypto_hash()}" ) ) return False return True def contest_is_valid_for_style( contest: CiphertextBallotContest, description: ContestDescriptionWithPlaceholders ) -> bool: """ Determine if contest is valid for ballot style :param contest: Contest :param description: Contest description :return: Is valid """ # verify the hash matches if contest.description_hash != description.crypto_hash(): log_warning( ( f"ballot is not valid for style: mismatched description hash {contest.description_hash} " f"for contest {description.object_id} hash {description.crypto_hash()}" ) ) return False # verify the placeholder count if len(contest.ballot_selections) != len(description.ballot_selections) + len( description.placeholder_selections ): log_warning( f"ballot is not valid for style: mismatched selection count for contest {description.object_id}" ) return False return True def ballot_is_valid_for_style( ballot: CiphertextBallot, internal_manifest: InternalManifest ) -> bool: """ Determine if ballot is valid for ballot style :param ballot: Ballot :param internal_manifest: Internal election description :return: Is valid """ descriptions = internal_manifest.get_contests_for(ballot.style_id) for description in descriptions: use_contest = None for contest in ballot.contests: if description.object_id == contest.object_id: use_contest = contest break # verify the contest exists on the ballot if use_contest is None: log_warning( f"ballot is not valid for style: missing contest {description.object_id}" ) return False if not contest_is_valid_for_style(use_contest, description): return False # verify the selection metadata for selection_description in description.ballot_selections: use_selection = None for selection in use_contest.ballot_selections: if selection_description.object_id == selection.object_id: use_selection = selection break if use_selection is None: log_warning( f"ballot is not valid for style: missing selection {selection_description.object_id}" ) return False if not selection_is_valid_for_style(use_selection, selection_description): return False return True ================================================ FILE: src/electionguard/big_integer.py ================================================ from typing import Any, Tuple, Union from base64 import b16decode # pylint: disable=no-name-in-module from gmpy2 import mpz from .utils import BYTE_ORDER def _hex_to_int(input: str) -> int: """Given a hex string representing bytes, returns an int.""" valid_bytes = input[1:] if (len(input) % 2 != 0 and input[0] == "0") else input hex_bytes = bytes.fromhex(valid_bytes) return int.from_bytes(hex_bytes, BYTE_ORDER) def _int_to_hex(input: int) -> str: """Given an int, returns a hex string representing bytes.""" def pad_hex(hex: str) -> str: """Pad hex to ensure 2 digit hexadecimal format maintained.""" return "0" + hex if len(hex) % 2 else hex hex = format(input, "02X") return pad_hex(hex) def bytes_to_hex(input: bytes) -> str: return _int_to_hex(int.from_bytes(input, BYTE_ORDER)) _zero = mpz(0) def _convert_to_element(data: Union[int, str]) -> Tuple[str, int]: """Convert element to consistent types""" if isinstance(data, str): integer = _hex_to_int(data) hex = _int_to_hex(integer) else: hex = _int_to_hex(data) integer = data return (hex, integer) class BigInteger(str): """A specialized representation of a big integer in python""" _value: mpz = _zero def __new__(cls, data: Union[int, str]): # type: ignore (hex, integer) = _convert_to_element(data) big_int = super(BigInteger, cls).__new__(cls, hex) big_int._value = mpz(integer) return big_int @property def value(self) -> mpz: """Get internal value for math calculations""" return self._value def __int__(self) -> int: """Overload int conversion.""" return int(self.value) def __eq__(self, other: Any) -> bool: """Overload == (equal to) operator.""" return ( isinstance(other, BigInteger) and int(self.value) == int(other.value) ) or (isinstance(other, int) and int(self.value) == other) def __ne__(self, other: Any) -> bool: """Overload != (not equal to) operator.""" return not self == other def __lt__(self, other: Any) -> bool: """Overload <= (less than) operator.""" return ( isinstance(other, BigInteger) and int(self.value) < int(other.value) ) or (isinstance(other, int) and int(self.value) < other) def __le__(self, other: Any) -> bool: """Overload <= (less than or equal) operator.""" return self.__lt__(other) or self.__eq__(other) def __gt__(self, other: Any) -> bool: """Overload > (greater than) operator.""" return ( isinstance(other, BigInteger) and int(self.value) > int(other.value) ) or (isinstance(other, int) and int(self.value) > other) def __ge__(self, other: Any) -> bool: """Overload >= (greater than or equal) operator.""" return self.__gt__(other) or self.__eq__(other) def __hash__(self) -> int: """Overload the hashing function.""" return hash(self.value) def to_hex(self) -> str: """ Convert from the element to the hex representation of bytes. """ return str(self) def to_hex_bytes(self) -> bytes: """ Convert from the element to the representation of bytes by first going through hex. """ return b16decode(self) ================================================ FILE: src/electionguard/byte_padding.py ================================================ from enum import IntEnum from typing import Literal _PAD_BYTE = b"\x00" _BYTE_ORDER: Literal["little", "big"] = "big" _PAD_INDICATOR_SIZE = 2 class DataSize(IntEnum): """Define the sizes for data.""" Bytes_512 = 512 class TruncationError(ValueError): """A specific truncation error to indicate when padded data is truncated.""" def to_padded_bytes(data: str, size: DataSize = DataSize.Bytes_512) -> bytes: """Returns the data field as bytes, padded to the correct size.""" data_bytes = bytes.fromhex(data) if len(data_bytes) >= size: return data_bytes padding_length = size - len(data_bytes) return bytes(padding_length) + data_bytes def add_padding( message: bytes, size: DataSize = DataSize.Bytes_512, allow_truncation: bool = False ) -> bytes: """Add padding to message in bytes.""" message_length = len(message) padded_data_size = size - _PAD_INDICATOR_SIZE if message_length > padded_data_size: if allow_truncation: message_length = padded_data_size else: raise TruncationError( "Padded data exceeds allowed padded data size of {padded_data_size}." ) padding_length = padded_data_size - message_length leading_byte = padding_length.to_bytes(_PAD_INDICATOR_SIZE, byteorder=_BYTE_ORDER) padded = leading_byte + message[:message_length] + _PAD_BYTE * padding_length return padded def remove_padding(padded: bytes, size: DataSize = DataSize.Bytes_512) -> bytes: """Remove padding from padded message in bytes.""" padding_length = int.from_bytes(padded[:_PAD_INDICATOR_SIZE], byteorder=_BYTE_ORDER) message_end = size - padding_length return padded[_PAD_INDICATOR_SIZE:message_end] ================================================ FILE: src/electionguard/chaum_pedersen.py ================================================ # pylint: disable=too-many-instance-attributes from dataclasses import dataclass from .elgamal import ElGamalCiphertext from .group import ( ElementModQ, ElementModP, g_pow_p, mult_p, pow_p, a_minus_b_q, a_plus_bc_q, add_q, negate_q, int_to_q, ZERO_MOD_Q, ) from .hash import hash_elems from .logs import log_warning from .nonces import Nonces from .proof import Proof, ProofUsage @dataclass class DisjunctiveChaumPedersenProof(Proof): """ Representation of disjunctive Chaum Pederson proof """ proof_zero_pad: ElementModP """a0 in the spec""" proof_zero_data: ElementModP """b0 in the spec""" proof_one_pad: ElementModP """a1 in the spec""" proof_one_data: ElementModP """b1 in the spec""" proof_zero_challenge: ElementModQ """c0 in the spec""" proof_one_challenge: ElementModQ """c1 in the spec""" challenge: ElementModQ """c in the spec""" proof_zero_response: ElementModQ """proof_zero_response in the spec""" proof_one_response: ElementModQ """proof_one_response in the spec""" usage: ProofUsage = ProofUsage.SelectionValue """a description of how to use this proof""" def __post_init__(self) -> None: super().__init__() def is_valid( self, message: ElGamalCiphertext, k: ElementModP, q: ElementModQ ) -> bool: """ Validates a "disjunctive" Chaum-Pedersen (zero or one) proof. :param message: The ciphertext message :param k: The public key of the election :param q: The extended base hash of the election :return: True if everything is consistent. False otherwise. """ alpha = message.pad beta = message.data a0 = self.proof_zero_pad b0 = self.proof_zero_data a1 = self.proof_one_pad b1 = self.proof_one_data c0 = self.proof_zero_challenge c1 = self.proof_one_challenge c = self.challenge v0 = self.proof_zero_response v1 = self.proof_one_response in_bounds_alpha = alpha.is_valid_residue() in_bounds_beta = beta.is_valid_residue() in_bounds_a0 = a0.is_valid_residue() in_bounds_b0 = b0.is_valid_residue() in_bounds_a1 = a1.is_valid_residue() in_bounds_b1 = b1.is_valid_residue() in_bounds_c0 = c0.is_in_bounds() in_bounds_c1 = c1.is_in_bounds() in_bounds_v0 = v0.is_in_bounds() in_bounds_v1 = v1.is_in_bounds() consistent_c = add_q(c0, c1) == c == hash_elems(q, alpha, beta, a0, b0, a1, b1) consistent_gv0 = g_pow_p(v0) == mult_p(a0, pow_p(alpha, c0)) consistent_gv1 = g_pow_p(v1) == mult_p(a1, pow_p(alpha, c1)) consistent_kv0 = pow_p(k, v0) == mult_p(b0, pow_p(beta, c0)) consistent_gc1kv1 = mult_p(g_pow_p(c1), pow_p(k, v1)) == mult_p( b1, pow_p(beta, c1) ) success = ( in_bounds_alpha and in_bounds_beta and in_bounds_a0 and in_bounds_b0 and in_bounds_a1 and in_bounds_b1 and in_bounds_c0 and in_bounds_c1 and in_bounds_v0 and in_bounds_v1 and consistent_c and consistent_gv0 and consistent_gv1 and consistent_kv0 and consistent_gc1kv1 ) if not success: log_warning( "found an invalid Disjunctive Chaum-Pedersen proof: " + str( { "in_bounds_alpha": in_bounds_alpha, "in_bounds_beta": in_bounds_beta, "in_bounds_a0": in_bounds_a0, "in_bounds_b0": in_bounds_b0, "in_bounds_a1": in_bounds_a1, "in_bounds_b1": in_bounds_b1, "in_bounds_c0": in_bounds_c0, "in_bounds_c1": in_bounds_c1, "in_bounds_v0": in_bounds_v0, "in_bounds_v1": in_bounds_v1, "consistent_c": consistent_c, "consistent_gv0": consistent_gv0, "consistent_gv1": consistent_gv1, "consistent_kv0": consistent_kv0, "consistent_gc1kv1": consistent_gc1kv1, "k": k, "proof": self, } ), ) return success @dataclass class ChaumPedersenProof(Proof): """ Representation of a generic Chaum-Pedersen Zero Knowledge proof """ pad: ElementModP """a in the spec""" data: ElementModP """b in the spec""" challenge: ElementModQ """c in the spec""" response: ElementModQ """v in the spec""" usage: ProofUsage = ProofUsage.SecretValue """a description of how to use this proof""" def __post_init__(self) -> None: super().__init__() def is_valid( self, message: ElGamalCiphertext, k: ElementModP, m: ElementModP, q: ElementModQ, ) -> bool: """ Validates a Chaum-Pedersen proof. e.g. - The given value 𝑣𝑖 is in the set Z𝑞 - The given values 𝑎𝑖 and 𝑏𝑖 are both in the set Z𝑞^𝑟 - The challenge value 𝑐 satisfies 𝑐 = 𝐻(𝑄, (𝐴, 𝐵), (𝑎 , 𝑏 ), 𝑀 ). - that the equations 𝑔^𝑣𝑖 = 𝑎𝑖𝐾^𝑐𝑖 mod 𝑝 and 𝐴^𝑣𝑖 = 𝑏𝑖𝑀𝑖^𝑐𝑖 mod 𝑝 are satisfied. :param message: The ciphertext message :param k: The public key corresponding to the private key used to encrypt (e.g. the Guardian public election key) :param m: The value being checked for validity :param q: The extended base hash of the election :return: True if everything is consistent. False otherwise. """ alpha = message.pad beta = message.data a = self.pad b = self.data c = self.challenge v = self.response in_bounds_alpha = alpha.is_valid_residue() in_bounds_beta = beta.is_valid_residue() in_bounds_k = k.is_valid_residue() in_bounds_m = m.is_valid_residue() in_bounds_a = a.is_valid_residue() in_bounds_b = b.is_valid_residue() in_bounds_c = c.is_in_bounds() in_bounds_v = v.is_in_bounds() in_bounds_q = q.is_in_bounds() same_c = c == hash_elems(q, alpha, beta, a, b, m) consistent_gv = ( in_bounds_v and in_bounds_a and in_bounds_c # The equation 𝑔^𝑣𝑖 = 𝑎𝑖𝐾^𝑐𝑖 and g_pow_p(v) == mult_p(a, pow_p(k, c)) ) # The equation 𝐴^𝑣𝑖 = 𝑏𝑖𝑀𝑖^𝑐𝑖 mod 𝑝 consistent_av = ( in_bounds_alpha and in_bounds_b and in_bounds_c and in_bounds_v and pow_p(alpha, v) == mult_p(b, pow_p(m, c)) ) success = ( in_bounds_alpha and in_bounds_beta and in_bounds_k and in_bounds_m and in_bounds_a and in_bounds_b and in_bounds_c and in_bounds_v and in_bounds_q and same_c and consistent_gv and consistent_av ) if not success: log_warning( "found an invalid Chaum-Pedersen proof: " + str( { "in_bounds_alpha": in_bounds_alpha, "in_bounds_beta": in_bounds_beta, "in_bounds_k": in_bounds_k, "in_bounds_m": in_bounds_m, "in_bounds_a": in_bounds_a, "in_bounds_b": in_bounds_b, "in_bounds_c": in_bounds_c, "in_bounds_v": in_bounds_v, "in_bounds_q": in_bounds_q, "same_c": same_c, "consistent_gv": consistent_gv, "consistent_av": consistent_av, "k": k, "q": q, "proof": self, } ), ) return success @dataclass class ConstantChaumPedersenProof(Proof): """ Representation of constant Chaum Pederson proof """ pad: ElementModP """a in the spec""" data: ElementModP "b in the spec" challenge: ElementModQ "c in the spec" response: ElementModQ "v in the spec" constant: int """constant value""" usage: ProofUsage = ProofUsage.SelectionLimit """a description of how to use this proof""" def __post_init__(self) -> None: super().__init__() def is_valid( self, message: ElGamalCiphertext, k: ElementModP, q: ElementModQ ) -> bool: """ Validates a "constant" Chaum-Pedersen proof. e.g. that the equations 𝑔𝑉 = 𝑎𝐴𝐶 mod 𝑝 and 𝑔𝐿𝐾𝑣 = 𝑏𝐵𝐶 mod 𝑝 are satisfied. :param message: The ciphertext message :param k: The public key of the election :param q: The extended base hash of the election :return: True if everything is consistent. False otherwise. """ alpha = message.pad beta = message.data a = self.pad b = self.data c = self.challenge v = self.response constant = self.constant in_bounds_alpha = alpha.is_valid_residue() in_bounds_beta = beta.is_valid_residue() in_bounds_a = a.is_valid_residue() in_bounds_b = b.is_valid_residue() in_bounds_c = c.is_in_bounds() in_bounds_v = v.is_in_bounds() tmp = int_to_q(constant) if tmp is None: constant_q = ZERO_MOD_Q in_bounds_constant = False else: constant_q = tmp in_bounds_constant = True # this is an arbitrary constant check to verify that decryption will be performant # in some use cases this value may need to be increased sane_constant = 0 <= constant < 1_000_000_000 same_c = c == hash_elems(q, alpha, beta, a, b) consistent_gv = ( in_bounds_v and in_bounds_a and in_bounds_alpha and in_bounds_c # The equation 𝑔^𝑉 = 𝑎𝐴^𝐶 mod 𝑝 and g_pow_p(v) == mult_p(a, pow_p(alpha, c)) ) # The equation 𝑔^𝐿𝐾^𝑣 = 𝑏𝐵^𝐶 mod 𝑝 consistent_kv = in_bounds_constant and mult_p( g_pow_p(mult_p(c, constant_q)), pow_p(k, v) ) == mult_p(b, pow_p(beta, c)) success = ( in_bounds_alpha and in_bounds_beta and in_bounds_a and in_bounds_b and in_bounds_c and in_bounds_v and same_c and in_bounds_constant and sane_constant and consistent_gv and consistent_kv ) if not success: log_warning( "found an invalid Constant Chaum-Pedersen proof: " + str( { "in_bounds_alpha": in_bounds_alpha, "in_bounds_beta": in_bounds_beta, "in_bounds_a": in_bounds_a, "in_bounds_b": in_bounds_b, "in_bounds_c": in_bounds_c, "in_bounds_v": in_bounds_v, "in_bounds_constant": in_bounds_constant, "sane_constant": sane_constant, "same_c": same_c, "consistent_gv": consistent_gv, "consistent_kv": consistent_kv, "k": k, "proof": self, } ), ) return success def make_disjunctive_chaum_pedersen( message: ElGamalCiphertext, r: ElementModQ, k: ElementModP, q: ElementModQ, seed: ElementModQ, plaintext: int, ) -> DisjunctiveChaumPedersenProof: """ Produce a "disjunctive" proof that an encryption of a given plaintext is either an encrypted zero or one. This is just a front-end helper for `make_disjunctive_chaum_pedersen_zero` and `make_disjunctive_chaum_pedersen_one`. :param message: An ElGamal ciphertext :param r: The nonce used creating the ElGamal ciphertext :param k: The ElGamal public key for the election :param q: A value used when generating the challenge, usually the election extended base hash (𝑄') :param seed: Used to generate other random values here :param plaintext: Zero or one """ assert ( 0 <= plaintext <= 1 ), "make_disjunctive_chaum_pedersen only supports plaintexts of 0 or 1" if plaintext == 0: return make_disjunctive_chaum_pedersen_zero(message, r, k, q, seed) return make_disjunctive_chaum_pedersen_one(message, r, k, q, seed) def make_disjunctive_chaum_pedersen_zero( message: ElGamalCiphertext, r: ElementModQ, k: ElementModP, q: ElementModQ, seed: ElementModQ, ) -> DisjunctiveChaumPedersenProof: """ Produces a "disjunctive" proof that an encryption of zero is either an encrypted zero or one. :param message: An ElGamal ciphertext :param r: The nonce used creating the ElGamal ciphertext :param k: The ElGamal public key for the election :param q: A value used when generating the challenge, usually the election extended base hash (𝑄') :param seed: Used to generate other random values here """ alpha = message.pad beta = message.data # Pick three random numbers in Q. c1, v, u0 = Nonces(seed, "disjoint-chaum-pedersen-proof")[0:3] # Compute the NIZKP a0 = g_pow_p(u0) b0 = pow_p(k, u0) a1 = g_pow_p(v) b1 = mult_p(pow_p(k, v), g_pow_p(c1)) c = hash_elems(q, alpha, beta, a0, b0, a1, b1) c0 = a_minus_b_q(c, c1) v0 = a_plus_bc_q(u0, c0, r) v1 = a_plus_bc_q(v, c1, r) return DisjunctiveChaumPedersenProof(a0, b0, a1, b1, c0, c1, c, v0, v1) def make_disjunctive_chaum_pedersen_one( message: ElGamalCiphertext, r: ElementModQ, k: ElementModP, q: ElementModQ, seed: ElementModQ, ) -> DisjunctiveChaumPedersenProof: """ Produces a "disjunctive" proof that an encryption of one is either an encrypted zero or one. :param message: An ElGamal ciphertext :param r: The nonce used creating the ElGamal ciphertext :param k: The ElGamal public key for the election :param q: A value used when generating the challenge, usually the election extended base hash (𝑄') :param seed: Used to generate other random values here """ alpha = message.pad beta = message.data # Pick three random numbers in Q. w, v, u1 = Nonces(seed, "disjoint-chaum-pedersen-proof")[0:3] # Compute the NIZKP a0 = g_pow_p(v) b0 = mult_p(pow_p(k, v), g_pow_p(w)) a1 = g_pow_p(u1) b1 = pow_p(k, u1) c = hash_elems(q, alpha, beta, a0, b0, a1, b1) c0 = negate_q(w) c1 = add_q(c, w) v0 = a_plus_bc_q(v, c0, r) v1 = a_plus_bc_q(u1, c1, r) return DisjunctiveChaumPedersenProof(a0, b0, a1, b1, c0, c1, c, v0, v1) def make_chaum_pedersen( message: ElGamalCiphertext, s: ElementModQ, m: ElementModP, seed: ElementModQ, hash_header: ElementModQ, ) -> ChaumPedersenProof: """ Produces a proof that a given value corresponds to a specific encryption. computes: 𝑀 =𝐴^𝑠𝑖 mod 𝑝 and 𝐾𝑖 = 𝑔^𝑠𝑖 mod 𝑝 :param message: An ElGamal ciphertext :param s: The nonce or secret used to derive the value :param m: The value we are trying to prove :param seed: Used to generate other random values here :param hash_header: A value used when generating the challenge, usually the election extended base hash (𝑄') """ alpha = message.pad beta = message.data # Pick one random number in Q. u = Nonces(seed, "constant-chaum-pedersen-proof")[0] a = g_pow_p(u) # 𝑔^𝑢𝑖 mod 𝑝 b = pow_p(alpha, u) # 𝐴^𝑢𝑖 mod 𝑝 c = hash_elems(hash_header, alpha, beta, a, b, m) # sha256(𝑄', A, B, a𝑖, b𝑖, 𝑀𝑖) v = a_plus_bc_q(u, c, s) # (𝑢𝑖 + 𝑐𝑖𝑠𝑖) mod 𝑞 return ChaumPedersenProof(a, b, c, v) def make_constant_chaum_pedersen( message: ElGamalCiphertext, constant: int, r: ElementModQ, k: ElementModP, seed: ElementModQ, hash_header: ElementModQ, ) -> ConstantChaumPedersenProof: """ Produces a proof that a given encryption corresponds to a specific total value. :param message: An ElGamal ciphertext :param constant: The plaintext constant value used to make the ElGamal ciphertext (L in the spec) :param r: The aggregate nonce used creating the ElGamal ciphertext :param k: The ElGamal public key for the election :param seed: Used to generate other random values here :param hash_header: A value used when generating the challenge, usually the election extended base hash (𝑄') """ alpha = message.pad beta = message.data # Pick one random number in Q. u = Nonces(seed, "constant-chaum-pedersen-proof")[0] a = g_pow_p(u) # 𝑔^𝑢𝑖 mod 𝑝 b = pow_p(k, u) # 𝐴^𝑢𝑖 mod 𝑝 c = hash_elems(hash_header, alpha, beta, a, b) # sha256(𝑄', A, B, a, b) v = a_plus_bc_q(u, c, r) return ConstantChaumPedersenProof(a, b, c, v, constant) ================================================ FILE: src/electionguard/constants.py ================================================ """Creating and managing mathematic constants for the election.""" from os import getenv from dataclasses import dataclass from enum import Enum from .big_integer import BigInteger @dataclass class ElectionConstants: """The constants for mathematical functions during the election.""" large_prime: BigInteger """large prime or p""" small_prime: BigInteger """small prime or q""" cofactor: BigInteger # (p - 1) / q """cofactor or r""" generator: BigInteger """generator or g""" # 2^r mod p def create_constants( large_prime: int, small_prime: int, cofactor: int, generator: int ) -> ElectionConstants: """Create constants for election.""" return ElectionConstants( BigInteger(large_prime), BigInteger(small_prime), BigInteger(cofactor), BigInteger(generator), ) # pylint: disable=line-too-long STANDARD_CONSTANTS = create_constants( 1044388881413152506691752710716624382579964249047383780384233483283953907971553643537729993126875883902173634017777416360502926082946377942955704498542097614841825246773580689398386320439747911160897731551074903967243883427132918813748016269754522343505285898816777211761912392772914485521155521641049273446207578961939840619466145806859275053476560973295158703823395710210329314709715239251736552384080845836048778667318931418338422443891025911884723433084701207771901944593286624979917391350564662632723703007964229849154756196890615252286533089643184902706926081744149289517418249153634178342075381874131646013444796894582106870531535803666254579602632453103741452569793905551901541856173251385047414840392753585581909950158046256810542678368121278509960520957624737942914600310646609792665012858397381435755902851312071248102599442308951327039250818892493767423329663783709190716162023529669217300939783171415808233146823000766917789286154006042281423733706462905243774854543127239500245873582012663666430583862778167369547603016344242729592244544608279405999759391099769165589722584216017468464576217318557948461765770700913220460557598574717173408252913596242281190298966500668625620138188265530628036538314433100326660047110143, 115792089237316195423570985008687907853269984665640564039457584007913129639747, # pow(2, 256) - 189 9019518416950528558373478086511232658951474842525520401496114928154304263969655687927867442562559311457926593510757267649063628681241064260953609180947464800958467390949485096429653122916928704841547265126247408167856620024815508684472819746384115369148322548696439327979752948311712506113890045287907335656308945630141969472484100558565879585476547782717283106837945923693806973017510492730838409381014701258202694245760602718602550739205297257940969992371799325870179746191672464736721424617639973324090288952006260483222894269928179970153634220390287255837625331668555933039199194619824375869291271098935000699785346405055160394688637074599519052655517388596327473273906029869030988064607361165803129718773877185415445291671089029845994683414682274353665003204293107284473196033588697845087556526514092678744031772226855409523354476737660407619436531080189837076164818131039104397776628128325247709678431023369197272126578394856752060591013812807437681624251867074769638052097737959472027002770963255207757153746376691827309573603635608169799503216990026029763868313819255248026666854405409059422844776556067163611304891154793770115766608153679099327786, 119359756198641231858139651428439585561105914902686985078252796680474637856752833978884422594516170665312423393830118608408063594508087813277769835084746883589963798527237870817233369094387978405585759195339509768803496494994109693743279157584139079471178850751266233150727771094796709619646350222242437970473900636242584673413224137139139346254912172628651028694427789523683070264102332413084663100402635889283790741342401259356660761075766365672754329863241692760862540151023800163269173550320623249398630247531924855997863109776955214403044727497968354022277828136634059011708099779241302941071701051050378539485717425482151777277387633806111112178267035315726401285294598397677116389893642725498831127977915200359151833767358091365292230363248410124916825814514852703770457024102738694375502049388804979035628232209959549199366986471874840784466132903083308458356458177839111623113116525230200791649979270165318729763550486200224695556789081331596212761936863634467236301450039399776963661755684863012396788149479256016157814129329192490798309248914535389650594573156725696657302152874510063002532052622638033113978672254680147128450265983503193865576932419282003012093526302631221491418211528781074474515924597472841036553107847, ) # TEST ONLY # These constants serve as sets of primes for future developers # Currently, all the sets are all valid but may break certain tests # As tests adapt, these constants can be used to speed up tests EXTRA_SMALL_TEST_CONSTANTS = create_constants(157, 13, 12, 16) SMALL_TEST_CONSTANTS = create_constants(503, 251, 2, 5) MEDIUM_TEST_CONSTANTS = create_constants(65267, 32633, 2, 3) LARGE_TEST_CONSTANTS = create_constants( 18446744073704586917, 65521, 281539415968996, 15463152587872997502 ) class PrimeOption(Enum): """Option for primes to determine election constants.""" Standard = "Standard" TestOnly = "TestOnly" def get_constants() -> ElectionConstants: """Get constants for the election by the option for the primes.""" env_option = getenv("PRIME_OPTION") option: PrimeOption = ( PrimeOption(env_option) if env_option is not None else PrimeOption.Standard ) option_map = { PrimeOption.Standard: STANDARD_CONSTANTS, PrimeOption.TestOnly: LARGE_TEST_CONSTANTS, } return option_map.get(option) or STANDARD_CONSTANTS def get_large_prime() -> int: return int(get_constants().large_prime.value) def get_small_prime() -> int: return int(get_constants().small_prime.value) def get_cofactor() -> int: return int(get_constants().cofactor.value) def get_generator() -> int: return int(get_constants().generator.value) ================================================ FILE: src/electionguard/data_store.py ================================================ from collections.abc import Mapping from typing import ( Dict, Generic, Iterable, Iterator, List, Optional, Tuple, TypeVar, ) _T = TypeVar("_T") _U = TypeVar("_U") class DataStore(Generic[_T, _U]): """ A lightweight convenience wrapper around a dictionary for data storage. This implementation defines the common interface used to access stored state elements. """ _store: Dict[_T, _U] def __init__(self) -> None: self._store = {} def __iter__(self) -> Iterator: return iter(self._store.items()) def all(self) -> List[_U]: """ Get all `SubmittedBallot` from the store """ return list(self._store.values()) def clear(self) -> None: """ Clear data from store """ self._store.clear() def get(self, key: _T) -> Optional[_U]: """ Get value in store :param key: key :return: value if found """ return self._store.get(key) def items(self) -> Iterable[Tuple[_T, _U]]: """ Gets all items in store as list :return: List of (key, value) """ return self._store.items() def keys(self) -> Iterable[_T]: """ Gets all keys in store as list :return: List of keys """ return self._store.keys() def __len__(self) -> int: """ Get length or count of store :return: Count in store """ return len(self._store) def pop(self, key: _T) -> Optional[_U]: """ Pop an object from the store if it exists. :param key: key """ if key in self._store: return self._store.pop(key) return None def set(self, key: _T, value: _U) -> None: """ Create or update a new value in store :param key: key :param value: value """ self._store[key] = value def values(self) -> Iterable[_U]: """ Gets all values in store as list :return: List of values """ return self._store.values() class ReadOnlyDataStore(Generic[_T, _U], Mapping): """ A readonly view to a Data store """ def __init__(self, data: DataStore[_T, _U]): self._data: DataStore[_T, _U] = data def __getitem__(self, key: _T) -> Optional[_U]: return self._data.get(key) def __len__(self) -> int: return len(self._data) def __iter__(self) -> Iterator: return iter(self._data.items()) def __eq__(self, other: object) -> bool: if not isinstance(other, ReadOnlyDataStore): return False return ReadOnlyDataStore.__eq__(self, other) ================================================ FILE: src/electionguard/decrypt_with_secrets.py ================================================ from typing import List, Optional from .ballot import ( CiphertextBallot, CiphertextBallotContest, CiphertextBallotSelection, PlaintextBallot, PlaintextBallotContest, PlaintextBallotSelection, ) from .elgamal import ElGamalPublicKey, ElGamalSecretKey from .group import ElementModQ from .logs import log_warning from .manifest import ( InternalManifest, ContestDescriptionWithPlaceholders, SelectionDescription, ) from .nonces import Nonces from .utils import get_optional # The Methods in this file can be used to decrypt values if private keys or nonces are known def decrypt_selection_with_secret( selection: CiphertextBallotSelection, description: SelectionDescription, public_key: ElGamalPublicKey, secret_key: ElGamalSecretKey, crypto_extended_base_hash: ElementModQ, suppress_validity_check: bool = False, ) -> Optional[PlaintextBallotSelection]: """ Decrypt the specified `CiphertextBallotSelection` within the context of the specified selection. :param selection: the selection to decrypt :param description: the qualified selection metadata :param public_key: the public key for the election (K) :param secret_key: the known secret key used to generate the public key for this election :param crypto_extended_base_hash: the extended base hash code (𝑄') for the election :param suppress_validity_check: do not validate the encryption prior to decrypting (useful for tests) """ if not suppress_validity_check and not selection.is_valid_encryption( description.crypto_hash(), public_key, crypto_extended_base_hash ): log_warning(f"selection: {selection.object_id} failed validity check") return None plaintext_vote = selection.ciphertext.decrypt(secret_key) # TODO: ISSUE #47: handle decryption of the extradata field if needed return PlaintextBallotSelection( selection.object_id, plaintext_vote, selection.is_placeholder_selection, ) def decrypt_selection_with_nonce( selection: CiphertextBallotSelection, description: SelectionDescription, public_key: ElGamalPublicKey, crypto_extended_base_hash: ElementModQ, nonce_seed: Optional[ElementModQ] = None, suppress_validity_check: bool = False, ) -> Optional[PlaintextBallotSelection]: """ Decrypt the specified `CiphertextBallotSelection` within the context of the specified selection. :param selection: the contest selection to decrypt :param description: the qualified selection metadata that may be a placeholder selection :param public_key: the public key for the election (K) :param crypto_extended_base_hash: the extended base hash code (𝑄') for the election :param nonce_seed: the optional nonce that was seeded to the encryption function. if no value is provided, the nonce field from the selection is used :param suppress_validity_check: do not validate the encryption prior to decrypting (useful for tests) """ if not suppress_validity_check and not selection.is_valid_encryption( description.crypto_hash(), public_key, crypto_extended_base_hash ): log_warning(f"selection: {selection.object_id} failed validity check") return None if nonce_seed is None: nonce = selection.nonce else: nonce_sequence = Nonces(description.crypto_hash(), nonce_seed) nonce = nonce_sequence[description.sequence_order] if nonce is None: log_warning( f"missing nonce value. decrypt could not derive a nonce value for selection {selection.object_id}" ) return None if selection.nonce is not None and nonce != selection.nonce: log_warning( f"decrypt could not verify a nonce value for selection {selection.object_id}" ) return None plaintext_vote = selection.ciphertext.decrypt_known_nonce(public_key, nonce) # TODO: ISSUE #35: encrypt/decrypt: handle decryption of the extradata field if needed return PlaintextBallotSelection( selection.object_id, plaintext_vote, selection.is_placeholder_selection, ) def decrypt_contest_with_secret( contest: CiphertextBallotContest, description: ContestDescriptionWithPlaceholders, public_key: ElGamalPublicKey, secret_key: ElGamalSecretKey, crypto_extended_base_hash: ElementModQ, suppress_validity_check: bool = False, remove_placeholders: bool = True, ) -> Optional[PlaintextBallotContest]: """ Decrypt the specified `CiphertextBallotContest` within the context of the specified contest. :param contest: the contest to decrypt :param description: the qualified contest metadata that includes placeholder selections :param public_key: the public key for the election (K) :param secret_key: the known secret key used to generate the public key for this election :param crypto_extended_base_hash: the extended base hash code (𝑄') for the election :param suppress_validity_check: do not validate the encryption prior to decrypting (useful for tests) :param remove_placeholders: filter out placeholder ciphertext selections after decryption """ if not suppress_validity_check and not contest.is_valid_encryption( description.crypto_hash(), public_key, crypto_extended_base_hash ): log_warning(f"contest: {contest.object_id} failed validity check") return None plaintext_selections: List[PlaintextBallotSelection] = [] for selection in contest.ballot_selections: selection_description = description.selection_for(selection.object_id) plaintext_selection = decrypt_selection_with_secret( selection, get_optional(selection_description), public_key, secret_key, crypto_extended_base_hash, suppress_validity_check, ) if plaintext_selection is not None: if ( not remove_placeholders or not plaintext_selection.is_placeholder_selection ): plaintext_selections.append(plaintext_selection) else: log_warning( f"decryption with secret failed for contest: {contest.object_id} selection: {selection.object_id}" ) return None return PlaintextBallotContest(contest.object_id, plaintext_selections) def decrypt_contest_with_nonce( contest: CiphertextBallotContest, description: ContestDescriptionWithPlaceholders, public_key: ElGamalPublicKey, crypto_extended_base_hash: ElementModQ, nonce_seed: Optional[ElementModQ] = None, suppress_validity_check: bool = False, remove_placeholders: bool = True, ) -> Optional[PlaintextBallotContest]: """ Decrypt the specified `CiphertextBallotContest` within the context of the specified contest. :param contest: the contest to decrypt :param description: the qualified contest metadata that includes placeholder selections :param public_key: the public key for the election (K) :param crypto_extended_base_hash: the extended base hash code (𝑄') for the election :param nonce_seed: the optional nonce that was seeded to the encryption function if no value is provided, the nonce field from the contest is used :param suppress_validity_check: do not validate the encryption prior to decrypting (useful for tests) :param remove_placeholders: filter out placeholder ciphertext selections after decryption """ if not suppress_validity_check and not contest.is_valid_encryption( description.crypto_hash(), public_key, crypto_extended_base_hash ): log_warning(f"contest: {contest.object_id} failed validity check") return None if nonce_seed is None: nonce_seed = contest.nonce else: nonce_sequence = Nonces(description.crypto_hash(), nonce_seed) nonce_seed = nonce_sequence[description.sequence_order] if nonce_seed is None: log_warning( f"missing nonce_seed value. decrypt could not dewrive a nonce value for contest {contest.object_id}" ) return None if contest.nonce is not None and nonce_seed != contest.nonce: log_warning( f"decrypt could not verify a nonce_seed value for contest {contest.object_id}" ) return None plaintext_selections: List[PlaintextBallotSelection] = [] for selection in contest.ballot_selections: selection_description = description.selection_for(selection.object_id) plaintext_selection = decrypt_selection_with_nonce( selection, get_optional(selection_description), public_key, crypto_extended_base_hash, nonce_seed, suppress_validity_check, ) if plaintext_selection is not None: if ( not remove_placeholders or not plaintext_selection.is_placeholder_selection ): plaintext_selections.append(plaintext_selection) else: log_warning( f"decryption with nonce failed for contest: {contest.object_id} selection: {selection.object_id}" ) return None return PlaintextBallotContest(contest.object_id, plaintext_selections) def decrypt_ballot_with_secret( ballot: CiphertextBallot, internal_manifest: InternalManifest, crypto_extended_base_hash: ElementModQ, public_key: ElGamalPublicKey, secret_key: ElGamalSecretKey, suppress_validity_check: bool = False, remove_placeholders: bool = True, ) -> Optional[PlaintextBallot]: """ Decrypt the specified `CiphertextBallot` within the context of the specified election. :param ballot: the ballot to decrypt :param internal_manifest: the qualified election metadata that includes placeholder selections :param crypto_extended_base_hash: the extended base hash code (𝑄') for the election :param public_key: the public key for the election (K) :param secret_key: the known secret key used to generate the public key for this election :param suppress_validity_check: do not validate the encryption prior to decrypting (useful for tests) :param remove_placeholders: filter out placeholder ciphertext selections after decryption """ if not suppress_validity_check and not ballot.is_valid_encryption( internal_manifest.manifest_hash, public_key, crypto_extended_base_hash ): log_warning(f"ballot: {ballot.object_id} failed validity check") return None plaintext_contests: List[PlaintextBallotContest] = [] for contest in ballot.contests: description = internal_manifest.contest_for(contest.object_id) plaintext_contest = decrypt_contest_with_secret( contest, get_optional(description), public_key, secret_key, crypto_extended_base_hash, suppress_validity_check, remove_placeholders, ) if plaintext_contest is not None: plaintext_contests.append(plaintext_contest) else: log_warning( f"decryption with nonce failed for ballot: {ballot.object_id} selection: {contest.object_id}" ) return None return PlaintextBallot(ballot.object_id, ballot.style_id, plaintext_contests) def decrypt_ballot_with_nonce( ballot: CiphertextBallot, internal_manifest: InternalManifest, crypto_extended_base_hash: ElementModQ, public_key: ElGamalPublicKey, nonce: Optional[ElementModQ] = None, suppress_validity_check: bool = False, remove_placeholders: bool = True, ) -> Optional[PlaintextBallot]: """ Decrypt the specified `CiphertextBallot` within the context of the specified election. :param ballot: the ballot to decrypt :param internal_manifest: the qualified election metadata that includes placeholder selections :param crypto_extended_base_hash: the extended base hash code (𝑄') for the election :param public_key: the public key for the election (K) :param nonce: the optional master ballot nonce that was either seeded to, or gernated by the encryption function :param suppress_validity_check: do not validate the encryption prior to decrypting (useful for tests) :param remove_placeholders: filter out placeholder ciphertext selections after decryption """ if not suppress_validity_check and not ballot.is_valid_encryption( internal_manifest.manifest_hash, public_key, crypto_extended_base_hash ): log_warning(f"ballot: {ballot.object_id} failed validity check") return None # Use the hashed representation included in the ballot # or override with the provided values if nonce is None: nonce_seed = ballot.hashed_ballot_nonce() else: nonce_seed = CiphertextBallot.nonce_seed( internal_manifest.manifest_hash, ballot.object_id, nonce ) if nonce_seed is None: log_warning( f"missing nonce_seed value. decrypt could not derive a nonce value for ballot {ballot.object_id}" ) return None plaintext_contests: List[PlaintextBallotContest] = [] for contest in ballot.contests: description = internal_manifest.contest_for(contest.object_id) plaintext_contest = decrypt_contest_with_nonce( contest, get_optional(description), public_key, crypto_extended_base_hash, nonce_seed, suppress_validity_check, remove_placeholders, ) if plaintext_contest is not None: plaintext_contests.append(plaintext_contest) else: log_warning( f"decryption with nonce failed for ballot: {ballot.object_id} selection: {contest.object_id}" ) return None return PlaintextBallot(ballot.object_id, ballot.style_id, plaintext_contests) ================================================ FILE: src/electionguard/decrypt_with_shares.py ================================================ from typing import Dict, Optional, Tuple from .ballot import SubmittedBallot, CiphertextContest, CiphertextSelection from .decryption_share import ( CiphertextDecryptionSelection, DecryptionShare, get_shares_for_selection, ) from .discrete_log import DiscreteLog from .group import ElementModP, ElementModQ, mult_p, div_p from .logs import log_warning from .manifest import ( ContestDescription, Manifest, ) from .tally import ( CiphertextTally, PlaintextTally, PlaintextTallyContest, PlaintextTallySelection, ) from .type import ContestId, GuardianId, SelectionId # The methods in this file can be used to decrypt values if private keys or nonces are not known # and the key ceremony is used to share secrets among a quorum of guardians def decrypt_tally( tally: CiphertextTally, shares: Dict[GuardianId, DecryptionShare], crypto_extended_base_hash: ElementModQ, manifest: Manifest, remove_placeholders: bool = True, ) -> Optional[PlaintextTally]: """ Try to decrypt the tally and the spoiled ballots using the provided decryption shares. :param tally: The CiphertextTally to decrypt :param shares: The guardian Decryption Shares for all guardians :param context: the Ciphertextelectioncontext :return: A PlaintextTally or None if there is an error """ contests: Dict[ContestId, PlaintextTallyContest] = {} contest_descriptions = { description.object_id: description for description in manifest.contests } for contest in tally.contests.values(): if contest.object_id not in contest_descriptions.keys(): continue # Skip contests not in manifest plaintext_contest = decrypt_contest_with_decryption_shares( CiphertextContest( contest.object_id, contest.sequence_order, contest.description_hash, list(contest.selections.values()), ), shares, crypto_extended_base_hash, contest_descriptions[contest.object_id], remove_placeholders, ) if not plaintext_contest: log_warning(f"contest: {contest.object_id} failed to decrypt with shares") return None contests[contest.object_id] = plaintext_contest return PlaintextTally(tally.object_id, contests) def decrypt_ballot( ballot: SubmittedBallot, shares: Dict[GuardianId, DecryptionShare], crypto_extended_base_hash: ElementModQ, manifest: Manifest, remove_placeholders: bool = True, ) -> Optional[PlaintextTally]: """ Try to decrypt a single ballot using the provided decryption shares. :param ballot: The SubmittedBallot to decrypt :param shares: The guardian Decryption Shares for all guardians :param crypto_extended_base_hash: The extended base hash :return: A PlaintextTally or None if there is an error """ contests: Dict[ContestId, PlaintextTallyContest] = {} contest_descriptions = { description.object_id: description for description in manifest.contests } for contest in ballot.contests: if contest.object_id not in contest_descriptions.keys(): continue # Skip contests not in manifest plaintext_contest = decrypt_contest_with_decryption_shares( CiphertextContest( contest.object_id, contest.sequence_order, contest.description_hash, contest.ballot_selections, ), shares, crypto_extended_base_hash, contest_descriptions[contest.object_id], remove_placeholders, ) if not plaintext_contest: log_warning(f"contest: {contest.object_id} failed to decrypt with shares") return None contests[contest.object_id] = plaintext_contest return PlaintextTally(ballot.object_id, contests) def decrypt_contest_with_decryption_shares( contest: CiphertextContest, shares: Dict[GuardianId, DecryptionShare], crypto_extended_base_hash: ElementModQ, contest_description: ContestDescription, remove_placeholders: bool = True, ) -> Optional[PlaintextTallyContest]: """ Decrypt the specified contest within the context of the specified Decryption Shares. :param contest: the contest to decrypt :param shares: a collection of `DecryptionShare` used to decrypt :param crypto_extended_base_hash: the extended base hash code (𝑄') for the election :return: a collection of `PlaintextTallyContest` or `None` if there is an error """ plaintext_selections: Dict[SelectionId, PlaintextTallySelection] = {} selection_description_ids = [ description.object_id for description in contest_description.ballot_selections ] for selection in contest.selections: if selection.object_id not in selection_description_ids and remove_placeholders: continue # Skip selections not in manifest (Such as placeholders) tally_shares = get_shares_for_selection(selection.object_id, shares) plaintext_selection = decrypt_selection_with_decryption_shares( selection, tally_shares, crypto_extended_base_hash ) if plaintext_selection is None: log_warning( ( f"could not decrypt contest {contest.object_id} " f"with selection {selection.object_id}" ) ) return None plaintext_selections[plaintext_selection.object_id] = plaintext_selection return PlaintextTallyContest(contest.object_id, plaintext_selections) def decrypt_selection_with_decryption_shares( selection: CiphertextSelection, shares: Dict[GuardianId, Tuple[ElementModP, CiphertextDecryptionSelection]], crypto_extended_base_hash: ElementModQ, suppress_validity_check: bool = False, ) -> Optional[PlaintextTallySelection]: """ Decrypt the specified `CiphertextSelection` with the collection of `ElementModP` decryption shares. Each share is expected to be passed with the corresponding public key so that the encryption can be validated :param selection: a `CiphertextSelection` :param shares: the collection of shares to decrypt the selection :param crypto_extended_base_hash: the extended base hash code (𝑄') for the election :param suppress_validity_check: do not validate the encryption prior to decrypting (useful for tests) :return: a `PlaintextTallySelection` or `None` if there is an error """ if not suppress_validity_check: # Verify that all of the shares are computed correctly for share in shares.values(): public_key, decryption = share # verify we have a proof or recovered parts if not decryption.is_valid( selection.ciphertext, public_key, crypto_extended_base_hash ): log_warning( f"share: {decryption.object_id} has invalid proof or recovered parts" ) return None # accumulate all of the shares calculated for the selection all_shares_product_M = mult_p( *[decryption.share for (_, decryption) in shares.values()] ) # Calculate 𝑀=𝐵⁄(∏𝑀𝑖) mod 𝑝. decrypted_value = div_p(selection.ciphertext.data, all_shares_product_M) d_log = DiscreteLog().discrete_log(decrypted_value) return PlaintextTallySelection( selection.object_id, d_log, decrypted_value, selection.ciphertext, [share for (guardian_id, (public_key, share)) in shares.items()], ) ================================================ FILE: src/electionguard/decryption.py ================================================ from typing import Dict, List, Optional, Tuple from electionguard.chaum_pedersen import ChaumPedersenProof, make_chaum_pedersen from electionguard.elgamal import ElGamalCiphertext from electionguard.utils import get_optional from .ballot import ( SubmittedBallot, CiphertextSelection, CiphertextContest, ) from .decryption_share import ( CiphertextDecryptionSelection, CiphertextCompensatedDecryptionSelection, CiphertextDecryptionContest, CiphertextCompensatedDecryptionContest, create_ciphertext_decryption_selection, DecryptionShare, CompensatedDecryptionShare, ) from .election import CiphertextElectionContext from .election_polynomial import compute_lagrange_coefficient from .group import ( ElementModP, ElementModQ, ONE_MOD_P, mult_p, pow_p, pow_q, rand_q, ) from .key_ceremony import ( CoordinateData, ElectionKeyPair, ElectionPartialKeyBackup, ElectionPublicKey, get_backup_seed, ) from .logs import log_warning from .scheduler import Scheduler from .tally import CiphertextTally from .type import ContestId, GuardianId, SelectionId RecoveryPublicKey = ElementModP def compute_decryption_share( key_pair: ElectionKeyPair, tally: CiphertextTally, context: CiphertextElectionContext, scheduler: Optional[Scheduler] = None, ) -> Optional[DecryptionShare]: """ Compute the decryption for all of the contests in the Ciphertext Tally :param guardian_keys: Guardian's election key pair :param tally: Encrypted tally to get decryption share of :param context: Election context :param scheduler: Scheduler :return: Return a guardian's decryption share of tally or None if error """ contests: Dict[ContestId, CiphertextDecryptionContest] = {} for contest in tally.contests.values(): contest_share = compute_decryption_share_for_contest( key_pair, CiphertextContest( contest.object_id, contest.sequence_order, contest.description_hash, list(contest.selections.values()), ), context, scheduler, ) if contest_share is None: return None contests[contest.object_id] = contest_share return DecryptionShare( tally.object_id, key_pair.owner_id, key_pair.key_pair.public_key, contests, ) def compute_compensated_decryption_share( missing_guardian_coordinate: ElementModQ, present_guardian_key: ElectionPublicKey, missing_guardian_key: ElectionPublicKey, tally: CiphertextTally, context: CiphertextElectionContext, scheduler: Optional[Scheduler] = None, ) -> Optional[CompensatedDecryptionShare]: """ Compute the compensated decryption for all of the contests in the Ciphertext Tally :param guardian_key: Guardian's election public key :param missing_guardian_key: Missing guardian's election public key :param missing_guardian_backup: Missing guardian's election partial key backup :param tally: Encrypted tally to get decryption share of :param context: Election context :param scheduler: Scheduler :return: Return a guardian's compensated decryption share of tally for the missing guardian or None if error """ contests: Dict[ContestId, CiphertextCompensatedDecryptionContest] = {} for contest in tally.contests.values(): contest_share = compute_compensated_decryption_share_for_contest( missing_guardian_coordinate, present_guardian_key, missing_guardian_key, CiphertextContest( contest.object_id, contest.sequence_order, contest.description_hash, list(contest.selections.values()), ), context, scheduler, ) if contest_share is None: return None contests[contest.object_id] = contest_share return CompensatedDecryptionShare( tally.object_id, present_guardian_key.owner_id, missing_guardian_key.owner_id, present_guardian_key.key, contests, ) def compute_decryption_share_for_ballot( key_pair: ElectionKeyPair, ballot: SubmittedBallot, context: CiphertextElectionContext, scheduler: Optional[Scheduler] = None, ) -> Optional[DecryptionShare]: """ Compute the decryption for a single ballot :param guardian_keys: Guardian's election key pair :param ballot: Ballot to be decrypted :param context: The public election encryption context :param scheduler: Scheduler :return: Decryption share for ballot or `None` if there is an error """ contests: Dict[ContestId, CiphertextDecryptionContest] = {} for contest in ballot.contests: contest_share = compute_decryption_share_for_contest( key_pair, CiphertextContest( contest.object_id, contest.sequence_order, contest.description_hash, contest.ballot_selections, ), context, scheduler, ) if contest_share is None: return None contests[contest.object_id] = contest_share return DecryptionShare( ballot.object_id, key_pair.owner_id, key_pair.share().key, contests, ) def compute_compensated_decryption_share_for_ballot( missing_guardian_coordinate: ElementModQ, missing_guardian_key: ElectionPublicKey, present_guardian_key: ElectionPublicKey, ballot: SubmittedBallot, context: CiphertextElectionContext, scheduler: Optional[Scheduler] = None, ) -> Optional[CompensatedDecryptionShare]: """ Compute the compensated decryption for a single ballot :param missing_guardian_coordinate: Missing guardian's election partial key backup :param missing_guardian_key: Missing guardian's election public key :param present_guardian_key: Present guardian's election public key :param ballot: Encrypted ballot to get decryption share of :param context: Election context :param scheduler: Scheduler :return: Return a guardian's compensated decryption share of ballot for the missing guardian or None if error """ contests: Dict[ContestId, CiphertextCompensatedDecryptionContest] = {} for contest in ballot.contests: contest_share = compute_compensated_decryption_share_for_contest( missing_guardian_coordinate, present_guardian_key, missing_guardian_key, CiphertextContest( contest.object_id, contest.sequence_order, contest.description_hash, contest.ballot_selections, ), context, scheduler, ) if contest_share is None: return None contests[contest.object_id] = contest_share return CompensatedDecryptionShare( ballot.object_id, present_guardian_key.owner_id, missing_guardian_key.owner_id, present_guardian_key.key, contests, ) def compute_decryption_share_for_contest( key_pair: ElectionKeyPair, contest: CiphertextContest, context: CiphertextElectionContext, scheduler: Optional[Scheduler] = None, ) -> Optional[CiphertextDecryptionContest]: """ Compute the decryption share for a single contest :param guardian_keys: Guardian's election key pair :param contest: Contest to be decrypted :param context: The public election encryption context :param scheduler: Scheduler :return: Decryption share for contest or `None` if there is an error """ if not scheduler: scheduler = Scheduler() selections: Dict[SelectionId, CiphertextDecryptionSelection] = {} decryptions: List[Optional[CiphertextDecryptionSelection]] = scheduler.schedule( compute_decryption_share_for_selection, [(key_pair, selection, context) for selection in contest.selections], with_shared_resources=True, ) for decryption in decryptions: if decryption is None: return None selections[decryption.object_id] = decryption return CiphertextDecryptionContest( contest.object_id, key_pair.owner_id, contest.description_hash, selections, ) def compute_compensated_decryption_share_for_contest( missing_guardian_coordinate: ElementModQ, present_guardian_key: ElectionPublicKey, missing_guardian_key: ElectionPublicKey, contest: CiphertextContest, context: CiphertextElectionContext, scheduler: Optional[Scheduler] = None, ) -> Optional[CiphertextCompensatedDecryptionContest]: """ Compute the compensated decryption share for a single contest :param missing_guardian_coordinate: Election partial key backup of the missing guardian :param guardian_key: The election public key of the available guardian that will partially decrypt the selection :param missing_guardian_key: Election public key of the guardian that is missing :param contest: The specific contest to decrypt :param context: The public election encryption context :return: a `CiphertextCompensatedDecryptionContest` or `None` if there is an error """ if not scheduler: scheduler = Scheduler() selections: Dict[SelectionId, CiphertextCompensatedDecryptionSelection] = {} selection_decryptions: List[Optional[CiphertextCompensatedDecryptionSelection]] = ( scheduler.schedule( compute_compensated_decryption_share_for_selection, [ ( missing_guardian_coordinate, present_guardian_key, missing_guardian_key, selection, context, ) for selection in contest.selections ], with_shared_resources=True, ) ) for decryption in selection_decryptions: if decryption is None: return None selections[decryption.object_id] = decryption return CiphertextCompensatedDecryptionContest( contest.object_id, present_guardian_key.owner_id, missing_guardian_key.owner_id, contest.description_hash, selections, ) def compute_decryption_share_for_selection( key_pair: ElectionKeyPair, selection: CiphertextSelection, context: CiphertextElectionContext, ) -> Optional[CiphertextDecryptionSelection]: """ Compute a partial decryption for a specific selection :param guardian_keys: Election keys for the guardian who will partially decrypt the selection :param selection: The specific selection to decrypt :param context: The public election encryption context :return: a `CiphertextDecryptionSelection` or `None` if there is an error """ (decryption, proof) = partially_decrypt( key_pair, selection.ciphertext, context.crypto_extended_base_hash ) if proof.is_valid( selection.ciphertext, key_pair.key_pair.public_key, decryption, context.crypto_extended_base_hash, ): return create_ciphertext_decryption_selection( selection.object_id, key_pair.owner_id, decryption, proof, ) log_warning( f"compute decryption share proof failed for guardian {key_pair.owner_id}" f"and {selection.object_id} with invalid proof" ) return None def compute_compensated_decryption_share_for_selection( missing_guardian_backup: ElementModQ, available_guardian_key: ElectionPublicKey, missing_guardian_key: ElectionPublicKey, selection: CiphertextSelection, context: CiphertextElectionContext, ) -> Optional[CiphertextCompensatedDecryptionSelection]: """ Compute a compensated decryption share for a specific selection using the available guardian's share of the missing guardian's private key polynomial :param missing_guardian_backup: The coordinate aka backup of a missing guardian :param available_guardian_key: Election public key of the guardian that is present :param missing_guardian_key: Election public key of the guardian that is missing :param selection: The specific selection to decrypt :param context: The public election encryption context :return: a `CiphertextCompensatedDecryptionSelection` or `None` if there is an error """ compensated = decrypt_with_threshold( missing_guardian_backup, selection.ciphertext, context.crypto_extended_base_hash, ) if compensated is None: log_warning( ( f"compute compensated decryption share failed for {available_guardian_key.owner_id} " f"missing: {missing_guardian_key.owner_id} {selection.object_id}" ) ) return None (decryption, proof) = compensated recovery_public_key = compute_recovery_public_key( available_guardian_key, missing_guardian_key ) if proof.is_valid( selection.ciphertext, recovery_public_key, decryption, context.crypto_extended_base_hash, ): share = CiphertextCompensatedDecryptionSelection( selection.object_id, available_guardian_key.owner_id, missing_guardian_key.owner_id, decryption, recovery_public_key, proof, ) return share log_warning( ( f"compute compensated decryption share proof failed for {available_guardian_key.owner_id} " f"missing: {missing_guardian_key.owner_id} {selection.object_id}" ) ) return None def partially_decrypt( key_pair: ElectionKeyPair, elgamal: ElGamalCiphertext, extended_base_hash: ElementModQ, nonce_seed: Optional[ElementModQ] = None, ) -> Tuple[ElementModP, ChaumPedersenProof]: """ Compute a partial decryption of an elgamal encryption :param elgamal: the `ElGamalCiphertext` that will be partially decrypted :param extended_base_hash: the extended base hash of the election that was used to generate t he ElGamal Ciphertext :param nonce_seed: an optional value used to generate the `ChaumPedersenProof` if no value is provided, a random number will be used. :return: a `Tuple[ElementModP, ChaumPedersenProof]` of the decryption and its proof """ if nonce_seed is None: nonce_seed = rand_q() # TODO: ISSUE #47: Decrypt the election secret key # 𝑀_i = 𝐴^𝑠𝑖 mod 𝑝 partial_decryption = elgamal.partial_decrypt(key_pair.key_pair.secret_key) # 𝑀_i = 𝐴^𝑠𝑖 mod 𝑝 and 𝐾𝑖 = 𝑔^𝑠𝑖 mod 𝑝 proof = make_chaum_pedersen( message=elgamal, s=key_pair.key_pair.secret_key, m=partial_decryption, seed=nonce_seed, hash_header=extended_base_hash, ) return (partial_decryption, proof) def decrypt_backup( guardian_backup: ElectionPartialKeyBackup, key_pair: ElectionKeyPair, ) -> Optional[ElementModQ]: """ Decrypts a compensated partial decryption of an elgamal encryption on behalf of a missing guardian :param guardian_backup: Missing guardian's backup :param key_pair: The present guardian's key pair that will be used to decrypt the backup :return: a `Tuple[ElementModP, ChaumPedersenProof]` of the decryption and its proof """ encryption_seed = get_backup_seed( key_pair.owner_id, key_pair.sequence_order, ) bytes_optional = guardian_backup.encrypted_coordinate.decrypt( key_pair.key_pair.secret_key, encryption_seed ) if bytes_optional is None: return None coordinate_data: CoordinateData = CoordinateData.from_bytes( get_optional(bytes_optional) ) return coordinate_data.coordinate def decrypt_with_threshold( coordinate: ElementModQ, ciphertext: ElGamalCiphertext, extended_base_hash: ElementModQ, nonce_seed: Optional[ElementModQ] = None, ) -> Optional[Tuple[ElementModP, ChaumPedersenProof]]: """ Compute a compensated partial decryption of an elgamal encryption given a coordinate from a missing guardian. :param coordinate: The coordinate aka backup provided to a present guardian from a missing guardian :param ciphertext: the `ElGamalCiphertext` that will be partially decrypted :param extended_base_hash: the extended base hash of the election that was used to generate the ElGamal Ciphertext :param nonce_seed: an optional value used to generate the `ChaumPedersenProof` if no value is provided, a random number will be used. :return: a `Tuple[ElementModP, ChaumPedersenProof]` of the decryption and its proof """ if nonce_seed is None: nonce_seed = rand_q() # 𝑀_{𝑖,l} = 𝐴^P𝑖_{l} partial_decryption = ciphertext.partial_decrypt(coordinate) # 𝑀_{𝑖,l} = 𝐴^𝑠𝑖 mod 𝑝 and 𝐾𝑖 = 𝑔^𝑠𝑖 mod 𝑝 proof = make_chaum_pedersen( ciphertext, coordinate, partial_decryption, nonce_seed, extended_base_hash, ) return (partial_decryption, proof) def compute_recovery_public_key( guardian_key: ElectionPublicKey, missing_guardian_key: ElectionPublicKey, ) -> RecoveryPublicKey: """ Compute the recovery public key, corresponding to the secret share Pi(l) K_ij^(l^j) for j in 0..k-1. K_ij is coefficients[j].public_key """ pub_key = ONE_MOD_P for index, commitment in enumerate(missing_guardian_key.coefficient_commitments): exponent = pow_q(guardian_key.sequence_order, index) pub_key = mult_p(pub_key, pow_p(commitment, exponent)) return pub_key def reconstruct_decryption_share( missing_guardian_key: ElectionPublicKey, tally: CiphertextTally, shares: Dict[GuardianId, CompensatedDecryptionShare], lagrange_coefficients: Dict[GuardianId, ElementModQ], ) -> DecryptionShare: """ Reconstruct the missing Decryption Share for a missing guardian from the collection of compensated decryption shares :param missing_guardian_id: The guardian id for the missing guardian :param public_key: The public key of the guardian creating share :param tally: The collection of `CiphertextTallyContest` that is cast :shares: the collection of `CompensatedTallyDecryptionShare` for the missing guardian from available guardians :lagrange_coefficients: the lagrange coefficients corresponding to the available guardians that provided shares """ contests: Dict[ContestId, CiphertextDecryptionContest] = {} for contest in tally.contests.values(): contests[contest.object_id] = reconstruct_decryption_contest( missing_guardian_key.owner_id, CiphertextContest( contest.object_id, contest.sequence_order, contest.description_hash, list(contest.selections.values()), ), shares, lagrange_coefficients, ) return DecryptionShare( tally.object_id, missing_guardian_key.owner_id, missing_guardian_key.key, contests, ) def reconstruct_decryption_share_for_ballot( missing_guardian_key: ElectionPublicKey, ballot: SubmittedBallot, shares: Dict[GuardianId, CompensatedDecryptionShare], lagrange_coefficients: Dict[GuardianId, ElementModQ], ) -> DecryptionShare: """ Reconstruct a missing ballot Decryption share for a missing guardian from the collection of compensated decryption shares :param missing_guardian_id: The guardian id for the missing guardian :param public_key: the public key for the missing guardian :param ballot: The `SubmittedBallot` to reconstruct :shares: the collection of `CompensatedBallotDecryptionShare` for the missing guardian, each keyed by the ID of the guardian that produced it from available guardians :lagrange_coefficients: the lagrange coefficients corresponding to the available guardians that provided shares """ contests: Dict[ContestId, CiphertextDecryptionContest] = {} for contest in ballot.contests: contests[contest.object_id] = reconstruct_decryption_contest( missing_guardian_key.owner_id, CiphertextContest( contest.object_id, contest.sequence_order, contest.description_hash, contest.ballot_selections, ), shares, lagrange_coefficients, ) return DecryptionShare( ballot.object_id, missing_guardian_key.owner_id, missing_guardian_key.key, contests, ) def reconstruct_decryption_contest( missing_guardian_id: GuardianId, contest: CiphertextContest, shares: Dict[GuardianId, CompensatedDecryptionShare], lagrange_coefficients: Dict[GuardianId, ElementModQ], ) -> CiphertextDecryptionContest: """ Reconstruct the missing Decryption Share for a missing guardian from the collection of compensated decryption shares :param missing_guardian_id: The guardian id for the missing guardian :param contest: The CiphertextContest to decrypt :shares: the collection of `CompensatedDecryptionShare` for the missing guardian from available guardians :lagrange_coefficients: the lagrange coefficients corresponding to the available guardians that provided shares """ contest_shares: Dict[GuardianId, CiphertextCompensatedDecryptionContest] = { available_guardian_id: compensated_share.contests[contest.object_id] for available_guardian_id, compensated_share in shares.items() } selections: Dict[SelectionId, CiphertextDecryptionSelection] = {} for selection in contest.selections: # collect all of the shares generated for each selection compensated_selection_shares: Dict[ GuardianId, CiphertextCompensatedDecryptionSelection ] = { available_guardian_id: compensated_contest.selections[selection.object_id] for available_guardian_id, compensated_contest in contest_shares.items() } share_pow_p = [] for available_guardian_id, share in compensated_selection_shares.items(): share_pow_p.append( pow_p(share.share, lagrange_coefficients[available_guardian_id]) ) reconstructed_share = mult_p(*share_pow_p) selections[selection.object_id] = create_ciphertext_decryption_selection( selection.object_id, missing_guardian_id, reconstructed_share, compensated_selection_shares, ) return CiphertextDecryptionContest( contest.object_id, missing_guardian_id, contest.description_hash, selections, ) def compute_lagrange_coefficients_for_guardians( available_guardians_keys: List[ElectionPublicKey], ) -> Dict[GuardianId, ElementModQ]: """ Produce all Lagrange coefficients for a collection of available Guardians, to be used when reconstructing a missing share. """ return { guardian_keys.owner_id: compute_lagrange_coefficients_for_guardian( guardian_keys, available_guardians_keys ) for guardian_keys in available_guardians_keys } def compute_lagrange_coefficients_for_guardian( guardian_key: ElectionPublicKey, other_guardians_keys: List[ElectionPublicKey], ) -> ElementModQ: """ Produce a Lagrange coefficient for a single Guardian, to be used when reconstructing a missing share. """ other_guardian_orders = [ g.sequence_order for g in other_guardians_keys if g.owner_id != guardian_key.owner_id ] return compute_lagrange_coefficient( guardian_key.sequence_order, *other_guardian_orders, ) ================================================ FILE: src/electionguard/decryption_mediator.py ================================================ from typing import Dict, List, Optional from .ballot import SubmittedBallot from .decryption import ( compute_lagrange_coefficients_for_guardians, reconstruct_decryption_share, reconstruct_decryption_share_for_ballot, ) from .decryption_share import DecryptionShare, CompensatedDecryptionShare from .decrypt_with_shares import decrypt_ballot, decrypt_tally from .election import CiphertextElectionContext from .group import ElementModQ from .key_ceremony import ElectionPublicKey from .key_ceremony_mediator import GuardianPair from .logs import log_info, log_warning from .manifest import Manifest from .tally import ( CiphertextTally, PlaintextTally, ) from .type import BallotId, GuardianId, MediatorId class DecryptionMediator: """ The Decryption Mediator composes partial decryptions from each Guardian to form a decrypted representation of an election tally. """ # pylint: disable=too-many-instance-attributes id: MediatorId _context: CiphertextElectionContext # Guardians _available_guardians: Dict[GuardianId, ElectionPublicKey] _missing_guardians: Dict[GuardianId, ElectionPublicKey] # Decryption Shares _tally_shares: Dict[GuardianId, DecryptionShare] _ballot_shares: Dict[BallotId, Dict[GuardianId, DecryptionShare]] # Compensated Shares _compensated_tally_shares: Dict[GuardianPair, CompensatedDecryptionShare] _compensated_ballot_shares: Dict[ BallotId, Dict[GuardianPair, CompensatedDecryptionShare] ] def __init__(self, id: MediatorId, context: CiphertextElectionContext): """Initialize the decryption mediator.""" self.id = id self._context = context self._available_guardians = {} self._missing_guardians = {} self._tally_shares = {} self._ballot_shares = {} self._compensated_tally_shares = {} self._compensated_ballot_shares = {} def announce( self, guardian_key: ElectionPublicKey, tally_share: DecryptionShare, ballot_shares: Optional[Dict[BallotId, Optional[DecryptionShare]]] = None, ) -> None: """ Announce that a Guardian is present and participating in the decryption. A guardian announces by presenting their id and their shares of the decryption :param guardian_key: The election public key of the guardian who will participate in the decryption. :param tally_share: Guardian's decryption share of the tally :param ballot_shares: Guardian's decryption shares of the ballots """ guardian_id = guardian_key.owner_id # Only allow a guardian to announce once if guardian_id in self._available_guardians: log_info(f"guardian {guardian_id} already announced") return self._save_tally_share(guardian_id, tally_share) if ballot_shares is not None: self._save_ballot_shares(guardian_id, ballot_shares) self._mark_available(guardian_key) def announce_missing(self, missing_guardian_key: ElectionPublicKey) -> None: """ Announce that a Guardian is missing and not participating in the decryption. :param missing_guardian_key: The election public key of the missing guardian """ missing_guardian_id = missing_guardian_key.owner_id # If guardian is available, can't be marked missing if missing_guardian_id in self._available_guardians: log_info(f"guardian {missing_guardian_id} already announced") return self._mark_missing(missing_guardian_key) def validate_missing_guardians( self, guardian_keys: List[ElectionPublicKey] ) -> bool: """Check the guardian's collections of keys and ensure the public keys match for the guardians.""" # Check this guardian's collection of public keys # for other guardians that have not announced missing_guardians: Dict[GuardianId, ElectionPublicKey] = { guardian_key.owner_id: guardian_key for guardian_key in guardian_keys if guardian_key.owner_id not in self._available_guardians } # Check that the public keys match for any missing guardians already reported # note this check naively assumes that the first guardian to annouce is telling the truth # but for this implementation it is simply a sanity check on the input data. # a consuming application should implement better validation of the guardian state # before announcing a guardian is available for decryption. for guardian_id, public_key in missing_guardians.items(): if guardian_id in self._missing_guardians: if self._missing_guardians[guardian_id] != public_key: log_warning( ( f"announce guardian: {guardian_id} " f"expected public key mismatch for missing {guardian_id}" ) ) return False else: self._missing_guardians[guardian_id] = missing_guardians[guardian_id] return True def announcement_complete(self) -> bool: """ Determine if the announcement phase is complete :return: True if announcement complete """ # If a quorum not announced, not ready if len(self._available_guardians) < self._context.quorum: log_warning("cannot decrypt with fewer than quorum available guardians") return False # If guardians missing or available not accounted for, not ready if ( len(self._available_guardians) + len(self._missing_guardians) != self._context.number_of_guardians ): log_warning( "cannot decrypt without accounting for all guardians missing or present" ) return False return True def get_available_guardians(self) -> List[ElectionPublicKey]: """ Get all available guardian keys :return: All available guardians election public keys """ return list(self._available_guardians.values()) def get_missing_guardians(self) -> List[ElectionPublicKey]: """ Get all missing guardian keys :return: All missing guardians election public keys """ return list(self._missing_guardians.values()) def receive_tally_compensation_share( self, tally_compensation_share: CompensatedDecryptionShare ) -> None: self._compensated_tally_shares[ GuardianPair( tally_compensation_share.guardian_id, tally_compensation_share.missing_guardian_id, ) ] = tally_compensation_share def receive_ballot_compensation_shares( self, ballot_compensation_shares: Dict[BallotId, CompensatedDecryptionShare] ) -> None: for ballot_id, share in ballot_compensation_shares.items(): ballot_shares = self._compensated_ballot_shares.get(ballot_id) if not ballot_shares: ballot_shares = {} ballot_shares[ GuardianPair(share.guardian_id, share.missing_guardian_id) ] = share self._compensated_ballot_shares[ballot_id] = ballot_shares def get_lagrange_coefficients(self) -> Dict[GuardianId, ElementModQ]: return compute_lagrange_coefficients_for_guardians( list(self._available_guardians.values()) ) def reconstruct_shares_for_tally(self, ciphertext_tally: CiphertextTally) -> None: lagrange_coefficients = self.get_lagrange_coefficients() for ( missing_guardian_id, missing_guardian_key, ) in self._missing_guardians.items(): # Share already reconstructed if missing_guardian_id in self._tally_shares: continue compensated_shares = _filter_by_missing_guardian( missing_guardian_id, self._compensated_tally_shares ) reconstructed_share = reconstruct_decryption_share( missing_guardian_key, ciphertext_tally, compensated_shares, lagrange_coefficients, ) # Add reconstructed share into tally shares self._tally_shares[missing_guardian_id] = reconstructed_share def reconstruct_shares_for_ballots( self, ciphertext_ballots: List[SubmittedBallot] ) -> None: lagrange_coefficients = compute_lagrange_coefficients_for_guardians( list(self._available_guardians.values()) ) for ciphertext_ballot in ciphertext_ballots: ballot_id = ciphertext_ballot.object_id ballot_shares = self._ballot_shares[ballot_id] for ( missing_guardian_id, missing_guardian_key, ) in self._missing_guardians.items(): # Share already reconstructed if missing_guardian_id in ballot_shares: continue compensated_shares = _filter_by_missing_guardian( missing_guardian_id, self._compensated_ballot_shares[ballot_id] ) reconstructed_share = reconstruct_decryption_share_for_ballot( missing_guardian_key, ciphertext_ballot, compensated_shares, lagrange_coefficients, ) ballot_shares[missing_guardian_id] = reconstructed_share # Add shares into ballot shares self._ballot_shares[ballot_id] = ballot_shares def get_plaintext_tally( self, ciphertext_tally: CiphertextTally, manifest: Manifest ) -> Optional[PlaintextTally]: """ Get the plaintext tally for the election by composing each Guardian's decrypted representation of each selection into a decrypted representation :return: a `PlaintextTally` or `None` """ if not self.announcement_complete() or not self._ready_to_decrypt( self._tally_shares ): return None return decrypt_tally( ciphertext_tally, self._tally_shares, self._context.crypto_extended_base_hash, manifest, ) def get_plaintext_ballots( self, ciphertext_ballots: List[SubmittedBallot], manifest: Manifest ) -> Optional[Dict[BallotId, PlaintextTally]]: """ Get the plaintext ballots for the election by composing each Guardian's decrypted representation of each selection into a decrypted representation This is typically used in the spoiled ballot use case. :return: a Plaintext Ballots or `None` """ if not self.announcement_complete(): return None ballots = {} for ciphertext_ballot in ciphertext_ballots: ballot_shares = self._ballot_shares.get(ciphertext_ballot.object_id) if not ballot_shares or not self._ready_to_decrypt(ballot_shares): # Skip ballot if not ready to decrypt continue ballot = decrypt_ballot( ciphertext_ballot, ballot_shares, self._context.crypto_extended_base_hash, manifest, ) if ballot: ballots[ballot.object_id] = ballot return ballots def _save_tally_share( self, guardian_id: GuardianId, guardians_tally_share: DecryptionShare ) -> None: """Save a guardians tally share.""" self._tally_shares[guardian_id] = guardians_tally_share def _save_ballot_shares( self, guardian_id: GuardianId, guardians_ballot_shares: Dict[BallotId, Optional[DecryptionShare]], ) -> None: """Save a guardian's set of ballot shares.""" for ballot_id, guardian_ballot_share in guardians_ballot_shares.items(): shares = self._ballot_shares.get(ballot_id) if shares is None: shares = {} if guardian_ballot_share is not None: shares[guardian_id] = guardian_ballot_share self._ballot_shares[ballot_id] = shares def _mark_available(self, guardian_key: ElectionPublicKey) -> None: """ This guardian removes itself from the missing list since it generated a valid share. """ guardian_id = guardian_key.owner_id self._available_guardians[guardian_id] = guardian_key if guardian_id in self._missing_guardians: self._missing_guardians.pop(guardian_id) def _mark_missing(self, guardian_key: ElectionPublicKey) -> None: """""" self._missing_guardians[guardian_key.owner_id] = guardian_key def _ready_to_decrypt(self, shares: Dict[GuardianId, DecryptionShare]) -> bool: """Shares are ready to decrypt.""" # If all guardian shares are represented including if necessary # the missing guardians reconstructed shares, the decryption can be made return len(shares) == self._context.number_of_guardians def _filter_by_missing_guardian( missing_guardian_id: GuardianId, shares: Dict[GuardianPair, CompensatedDecryptionShare], ) -> Dict[GuardianId, CompensatedDecryptionShare]: """ Filter a guardian pair and compensated share dictionary by missing guardian. """ missing_guardian_shares = {} for pair, share in shares.items(): if pair.designated_id == missing_guardian_id: missing_guardian_shares[pair.owner_id] = share return missing_guardian_shares ================================================ FILE: src/electionguard/decryption_share.py ================================================ from dataclasses import dataclass, field from typing import Dict, Optional, Tuple, Union from .chaum_pedersen import ChaumPedersenProof from .election_object_base import ElectionObjectBase from .elgamal import ElGamalCiphertext, ElGamalPublicKey from .group import ElementModP, ElementModQ from .logs import log_warning from .type import ContestId, GuardianId, SelectionId @dataclass class CiphertextCompensatedDecryptionSelection(ElectionObjectBase): """ A compensated fragment of a Guardian's Partial Decryption of a selection generated by an available guardian """ guardian_id: GuardianId """ The Available Guardian that this share belongs to """ missing_guardian_id: GuardianId """ The Missing Guardian for whom this share is calculated on behalf of """ share: ElementModP """ The Share of the decryption of a selection. `M_{i,l} in the spec` """ recovery_key: ElementModP """ The Recovery Public Key for the missing_guardian that corresponds to the available guardian's share of the secret """ proof: ChaumPedersenProof """ The Proof that the share was decrypted correctly """ ProofOrRecovery = Union[ ChaumPedersenProof, Dict[GuardianId, CiphertextCompensatedDecryptionSelection] ] @dataclass class CiphertextDecryptionSelection(ElectionObjectBase): """ A Guardian's Partial Decryption of a selection. A CiphertextDecryptionSelection can be generated by a guardian directly, or it can be compensated for by a quoprum of guardians When the guardian generates this share directly, the `proof` field is populated with a `chaumPedersen` proof that the decryption share was generated correctly. When the share is generated on behalf of this guardian by other guardians, the `recovered_parts` collection is populated with the `CiphertextCompensatedDecryptionSelection` objects generated by each available guardian. """ guardian_id: GuardianId """ The Available Guardian that this share belongs to """ share: ElementModP """ The Share of the decryption of a selection. `M_i` in the spec """ proof: Optional[ChaumPedersenProof] = field(init=True, default=None) """ The Proof that the share was decrypted correctly, if the guardian was available for decryption """ recovered_parts: Optional[ Dict[GuardianId, CiphertextCompensatedDecryptionSelection] ] = field(init=True, default=None) """ the recovered parts of the decryption provided by available guardians, if the guardian was missing from decryption """ def is_valid( self, message: ElGamalCiphertext, election_public_key: ElGamalPublicKey, extended_base_hash: ElementModQ, ) -> bool: """ Verify that this CiphertextDecryptionSelection is valid for a specific ElGamal key pair, public key, and election context. :param message: the `ElGamalCiphertext` to compare :param election_public_key: the `ElementModP Election Public Key for the Guardian :param extended_base_hash: The `ElementModQ` election extended base hash. """ # verify we have a proof or recovered parts if self.proof is None and self.recovered_parts is None: log_warning( ( f"CiphertextDecryptionSelection is_valid failed for guardian: {self.guardian_id} " f"selection: {self.object_id} with missing data" ) ) return False if self.proof is not None and self.recovered_parts is not None: log_warning( ( f"CiphertextDecryptionSelection is_valid failed for guardian: {self.guardian_id} " f"selection: {self.object_id} cannot have proof and recovery" ) ) return False if self.proof is not None and not self.proof.is_valid( message, election_public_key, self.share, extended_base_hash, ): log_warning( ( f"CiphertextDecryptionSelection is_valid failed for guardian: {self.guardian_id} " f"selection: {self.object_id} with invalid proof" ) ) return False if self.recovered_parts is not None: for ( _compensating_guardian_id, part, ) in self.recovered_parts.items(): if not part.proof.is_valid( message, part.recovery_key, part.share, extended_base_hash, ): log_warning( ( f"CiphertextDecryptionSelection is_valid failed for guardian: {self.guardian_id} " f"selection: {self.object_id} with invalid partial proof" ) ) return False return True def create_ciphertext_decryption_selection( object_id: str, guardian_id: GuardianId, share: ElementModP, proof_or_recovery: ProofOrRecovery, ) -> CiphertextDecryptionSelection: """ Create a ciphertext decryption selection :param object_id: Object id :param guardian_id: Guardian id :param description_hash: Description hash :param share: Share :param proof_or_recovery: Proof or recovery """ if isinstance(proof_or_recovery, ChaumPedersenProof): return CiphertextDecryptionSelection( object_id, guardian_id, share, proof=proof_or_recovery ) if isinstance(proof_or_recovery, dict): return CiphertextDecryptionSelection( object_id, guardian_id, share, recovered_parts=proof_or_recovery, ) log_warning(f"decryption share cannot assign {proof_or_recovery}") return CiphertextDecryptionSelection( object_id, guardian_id, share, ) @dataclass class CiphertextDecryptionContest(ElectionObjectBase): """ A Guardian's Partial Decryption of a contest """ guardian_id: GuardianId """ The Available Guardian that this share belongs to """ description_hash: ElementModQ """ The ContestDescription Hash """ selections: Dict[SelectionId, CiphertextDecryptionSelection] """ the collection of decryption shares for this contest's selections """ @dataclass class CiphertextCompensatedDecryptionContest(ElectionObjectBase): """ A Guardian's Partial Decryption of a contest """ guardian_id: GuardianId """ The Available Guardian that this share belongs to """ missing_guardian_id: GuardianId """ The Missing Guardian for whom this share is calculated on behalf of """ description_hash: ElementModQ """ The ContestDescription Hash """ selections: Dict[SelectionId, CiphertextCompensatedDecryptionSelection] """ the collection of decryption shares for this contest's selections """ @dataclass class DecryptionShare(ElectionObjectBase): """ A Guardian's Partial Decryption Share of a specific set of contests (Tally or Ballot) """ guardian_id: GuardianId """ The Available Guardian that this share belongs to """ public_key: ElGamalPublicKey """ The election public key for the guardian """ contests: Dict[ContestId, CiphertextDecryptionContest] """ The collection of all contests in the ballot """ @dataclass class CompensatedDecryptionShare(ElectionObjectBase): """ A Compensated Partial Decryption Share generated by an available guardian on behalf of a missing guardian """ guardian_id: GuardianId """ The Available Guardian that this share belongs to """ missing_guardian_id: GuardianId """ The Missing Guardian for whom this share is calculated on behalf of """ public_key: ElGamalPublicKey """ The election public key for the guardian """ contests: Dict[ContestId, CiphertextCompensatedDecryptionContest] """ The collection of all contests in the ballot """ def get_shares_for_selection( selection_id: str, shares: Dict[GuardianId, DecryptionShare], ) -> Dict[GuardianId, Tuple[ElementModP, CiphertextDecryptionSelection]]: """ Get all of the cast shares for a specific selection """ selections: Dict[GuardianId, Tuple[ElementModP, CiphertextDecryptionSelection]] = {} for share in shares.values(): for contest in share.contests.values(): for selection in contest.selections.values(): if selection.object_id == selection_id: selections[share.guardian_id] = (share.public_key, selection) return selections ================================================ FILE: src/electionguard/discrete_log.py ================================================ # pylint: disable=global-statement # support for computing discrete logs, with a cache so they're never recomputed import asyncio from typing import Dict, Tuple, Optional from .constants import get_generator from .singleton import Singleton from .group import BaseElement, ElementModP, ONE_MOD_P, mult_p DiscreteLogCache = Dict[ElementModP, int] _DLOG_MAX_EXPONENT = 100_000_000 """The max exponent to calculate. This value is used to stop a race condition.""" _INITIAL_CACHE = {ONE_MOD_P: 0} class DiscreteLogExponentError(ValueError): """Raised when the max exponent is larger than the system allows.""" def __init__(self, exponent: int, max_exponent: int = _DLOG_MAX_EXPONENT) -> None: super().__init__( f"Discrete log exponent of {exponent} exceeds maximum of {max_exponent}." ) class DiscreteLogNotFoundError(ValueError): """Raised when the discrete value could not be found in cache.""" def __init__(self, element: BaseElement) -> None: super().__init__(f"Discrete log of {element} could not be found in cache.") def compute_discrete_log( element: ElementModP, cache: DiscreteLogCache, max_exponent: int = _DLOG_MAX_EXPONENT, lazy_evaluation: bool = True, ) -> Tuple[int, DiscreteLogCache]: """ Computes the discrete log (base g, mod p) of the given element, with internal caching of results. Should run efficiently when called multiple times when the exponent is at most in the single-digit millions. Performance will degrade if it's much larger. For the best possible performance, pre-compute the discrete log of a number you expect to have the biggest exponent you'll ever see. After that, the cache will be fully loaded, and every call will be nothing more than a dictionary lookup. """ if element in cache: return (cache[element], cache) if not lazy_evaluation: raise DiscreteLogNotFoundError(element) _cache = compute_discrete_log_cache(element, cache, max_exponent) return (_cache[element], _cache) async def compute_discrete_log_async( element: ElementModP, cache: DiscreteLogCache, mutex: asyncio.Lock = asyncio.Lock(), max_exponent: int = _DLOG_MAX_EXPONENT, lazy_evaluation: bool = True, ) -> Tuple[int, DiscreteLogCache]: """ Computes the discrete log (base g, mod p) of the given element, with internal caching of results. Should run efficiently when called multiple times when the exponent is at most in the single-digit millions. Performance will degrade if it's much larger. Note: *this function is thread-safe*. For the best possible performance, pre-compute the discrete log of a number you expect to have the biggest exponent you'll ever see. After that, the cache will be fully loaded, and every call will be nothing more than a dictionary lookup. """ if element in cache: return (cache[element], cache) async with mutex: if element in cache: return (cache[element], cache) if not lazy_evaluation: raise DiscreteLogNotFoundError(element) _cache = compute_discrete_log_cache(element, cache, max_exponent) return (_cache[element], _cache) def precompute_discrete_log_cache( max_exponent: int, cache: Optional[DiscreteLogCache] = None ) -> DiscreteLogCache: """ Precompute the discrete log by the max exponent. """ if max_exponent > _DLOG_MAX_EXPONENT: raise DiscreteLogExponentError(max_exponent) if not cache: cache = _INITIAL_CACHE current_element = list(cache)[-1] prev_exponent = cache[current_element] if prev_exponent >= max_exponent: return cache g = ElementModP(get_generator(), False) for exponent in range(prev_exponent + 1, max_exponent + 1): current_element = mult_p(g, current_element) cache[current_element] = exponent return cache def compute_discrete_log_cache( element: ElementModP, cache: DiscreteLogCache, max_exponent: int = _DLOG_MAX_EXPONENT, ) -> DiscreteLogCache: """ Compute or lazy evaluation a discrete log cache up to the specified element. """ if max_exponent > _DLOG_MAX_EXPONENT: raise DiscreteLogExponentError(max_exponent) if not cache: cache = _INITIAL_CACHE max_element = list(cache)[-1] exponent = cache[max_element] if exponent > max_exponent: raise DiscreteLogExponentError(exponent, max_exponent) g = ElementModP(get_generator(), False) while element != max_element: exponent = exponent + 1 if exponent > max_exponent: raise DiscreteLogExponentError(exponent, max_exponent) max_element = mult_p(g, max_element) cache[max_element] = exponent return cache class DiscreteLog(Singleton): """ A class instance of the discrete log that includes a cache. """ _cache: DiscreteLogCache = {ONE_MOD_P: 0} _mutex = asyncio.Lock() _max_exponent: int = _DLOG_MAX_EXPONENT _lazy_evaluation: bool = True def get_cache(self) -> DiscreteLogCache: return self._cache def set_max_exponent(self, max_exponent: int) -> None: self._max_exponent = max_exponent def set_lazy_evaluation(self, lazy_evaluation: bool) -> None: self._lazy_evaluation = lazy_evaluation def precompute_cache(self, exponent: int) -> None: exponent = min(exponent, self._max_exponent) precompute_discrete_log_cache(exponent, self._cache) async def precompute_cache_async(self, exponent: int) -> None: exponent = min(exponent, self._max_exponent) async with self._mutex: precompute_discrete_log_cache(exponent) def discrete_log(self, element: ElementModP) -> int: (result, _cache) = compute_discrete_log( element, self._cache, self._max_exponent, self._lazy_evaluation ) return result async def discrete_log_async(self, element: ElementModP) -> int: (result, _cache) = await compute_discrete_log_async( element, self._cache, self._mutex, self._max_exponent, self._lazy_evaluation ) return result ================================================ FILE: src/electionguard/election.py ================================================ """Context for election encryption.""" from dataclasses import dataclass, field from typing import Dict, Optional from .constants import get_small_prime, get_large_prime, get_generator from .elgamal import ElGamalPublicKey from .group import ( ElementModQ, ElementModP, ) from .hash import hash_elems @dataclass class Configuration: """Configuration of election to allow edge cases.""" allow_overvotes: bool = field(default=True) """ Allow overvotes, votes exceeding selection limit, for the election. """ max_votes: int = field(default=1_000_000) """ Maximum votes, the maximum votes allowed on a selection for an aggregate ballot or tally. This can also be seen as the maximum ballots where a selection on a ballot can only have one vote. """ # pylint: disable=too-many-instance-attributes @dataclass(eq=True, unsafe_hash=True) class CiphertextElectionContext: """`CiphertextElectionContext` is the ElectionGuard representation of a specific election. Note: The ElectionGuard Data Spec deviates from the NIST model in that this object includes fields that are populated in the course of encrypting an election Specifically, `crypto_base_hash`, `crypto_extended_base_hash` and `elgamal_public_key` are populated with election-specific information necessary for encrypting the election. Refer to the specification for more information. To make an instance of this class, don't construct it directly. Use `make_ciphertext_election_context` instead. """ number_of_guardians: int """ The number of guardians necessary to generate the public key """ quorum: int """ The quorum of guardians necessary to decrypt an election. Must be fewer than `number_of_guardians` """ elgamal_public_key: ElGamalPublicKey """the `joint public key (K)` in the specification""" commitment_hash: ElementModQ """ the `commitment hash H(K 1,0 , K 2,0 ... , K n,0 )` of the public commitments guardians make to each other in the specification """ manifest_hash: ElementModQ """The hash of the election metadata""" crypto_base_hash: ElementModQ """The `base hash code (𝑄)` in the specification""" crypto_extended_base_hash: ElementModQ """The `extended base hash code (𝑄')` in specification""" extended_data: Optional[Dict[str, str]] """Data to allow extending the context for special cases.""" configuration: Configuration = field(default_factory=Configuration) """Configuration for the election edge cases.""" def get_extended_data_field(self, field_name: str) -> Optional[str]: """Returns the value for a field in the extended data or None if it isn't initialized.""" if self.extended_data is None: return None return self.extended_data.get(field_name) def make_ciphertext_election_context( number_of_guardians: int, quorum: int, elgamal_public_key: ElGamalPublicKey, commitment_hash: ElementModQ, manifest_hash: ElementModQ, extended_data: Optional[Dict[str, str]] = None, ) -> CiphertextElectionContext: """ Makes a CiphertextElectionContext object. :param number_of_guardians: The number of guardians necessary to generate the public key :param quorum: The quorum of guardians necessary to decrypt an election. Must be fewer than `number_of_guardians` :param elgamal_public_key: the public key of the election :param commitment_hash: the hash of the commitments the guardians make to each other :param manifest_hash: the hash of the election metadata """ # What's a crypto_base_hash? # The metadata of this object are hashed together with the # - prime modulus (𝑝), # - subgroup order (𝑞), # - generator (𝑔), # - number of guardians (𝑛), # - decryption threshold value (𝑘), # to form a base hash code (𝑄) which will be incorporated # into every subsequent hash computation in the election. # What's a crypto_extended_base_hash? # Once the baseline parameters have been produced and confirmed, # all of the public guardian commitments 𝐾𝑖,𝑗 are hashed together # with the base hash 𝑄 to form an extended base hash 𝑄' that will # form the basis of subsequent hash computations. crypto_base_hash = hash_elems( ElementModP(get_large_prime(), False), ElementModQ(get_small_prime(), False), ElementModP(get_generator(), False), number_of_guardians, quorum, manifest_hash, ) crypto_extended_base_hash = hash_elems(crypto_base_hash, commitment_hash) return CiphertextElectionContext( number_of_guardians, quorum, elgamal_public_key, commitment_hash, manifest_hash, crypto_base_hash, crypto_extended_base_hash, extended_data, ) ================================================ FILE: src/electionguard/election_object_base.py ================================================ """Base objects to derive other election objects.""" from dataclasses import dataclass from typing import List, Sequence, TypeVar @dataclass class ElectionObjectBase: """A base object to derive other election objects identifiable by object_id.""" object_id: str @dataclass class OrderedObjectBase(ElectionObjectBase): """A ordered base object to derive other election objects.""" sequence_order: int """ Used for ordering in a ballot to ensure various encryption primitives are deterministic. The sequence order must be unique and should be representative of how the items are represented on a template ballot in an external system. The sequence order is not required to be in the order in which they are displayed to a voter. Any acceptable range of integer values may be provided. """ _Orderable_T = TypeVar("_Orderable_T", bound="OrderedObjectBase") def sequence_order_sort(unsorted: List[_Orderable_T]) -> List[_Orderable_T]: """Sort by sequence order.""" return sorted(unsorted, key=lambda item: item.sequence_order) def list_eq( list1: Sequence[ElectionObjectBase], list2: Sequence[ElectionObjectBase] ) -> bool: """ We want to compare lists of election objects as if they're sets. We fake this by first sorting them on their object ids, then using regular list comparison. """ return sorted(list1, key=lambda x: x.object_id) == sorted( list2, key=lambda x: x.object_id ) ================================================ FILE: src/electionguard/election_polynomial.py ================================================ from dataclasses import dataclass from typing import Dict, List, Optional from .elgamal import ElGamalKeyPair from .group import ( add_q, ElementModP, ElementModQ, g_pow_p, div_q, mult_p, mult_q, ONE_MOD_P, pow_p, pow_q, rand_q, ZERO_MOD_Q, ) from .schnorr import make_schnorr_proof, SchnorrProof from .type import GuardianId SecretCoefficient = ElementModQ # Secret coefficient of election polynomial PublicCommitment = ElementModP # Public commitment of election polynomial @dataclass class Coefficient: """ A coefficient of an Election Polynomial """ value: SecretCoefficient """The secret coefficient `a_ij` """ commitment: PublicCommitment """The public key `K_ij` generated from secret coefficient""" proof: SchnorrProof """A proof of possession of the private key for the secret coefficient""" @dataclass class ElectionPolynomial: """ A polynomial defined by coefficients The 0-index coefficient is used for a secret key which can be discovered by a quorum of n guardians corresponding to n coefficients. """ coefficients: List[Coefficient] """List of coefficient value, commitments and proofs""" def get_commitments(self) -> List[PublicCommitment]: """Access the list of public keys generated from secret coefficient""" return [coefficient.commitment for coefficient in self.coefficients] def get_proofs(self) -> List[SchnorrProof]: """Access the list of proof of possesion of the private key for the secret coefficient""" return [coefficient.proof for coefficient in self.coefficients] def generate_polynomial( number_of_coefficients: int, nonce: Optional[ElementModQ] = None ) -> ElectionPolynomial: """ Generates a polynomial for sharing election keys :param number_of_coefficients: Number of coefficients of polynomial :param nonce: an optional nonce parameter that may be provided (useful for testing) :return: Polynomial used to share election keys """ coefficients: List[Coefficient] = [] for i in range(number_of_coefficients): # Note: the nonce value is not safe. it is designed for testing only. # this method should be called without the nonce in production. value = add_q(nonce, i) if nonce is not None else rand_q() commitment = g_pow_p(value) proof = make_schnorr_proof( ElGamalKeyPair(value, commitment), rand_q() ) # TODO Alternate schnoor proof method that doesn't need KeyPair coefficient = Coefficient(value, commitment, proof) coefficients.append(coefficient) return ElectionPolynomial(coefficients) def compute_polynomial_coordinate( exponent_modifier: int, polynomial: ElectionPolynomial ) -> ElementModQ: """ Computes a single coordinate value of the election polynomial used for sharing :param exponent_modifier: Unique modifier (usually sequence order) for exponent :param polynomial: Election polynomial :return: Polynomial used to share election keys """ exponent_modifier_mod_q = ElementModQ(exponent_modifier) computed_value = ZERO_MOD_Q for i, coefficient in enumerate(polynomial.coefficients): exponent = pow_q(exponent_modifier_mod_q, i) factor = mult_q(coefficient.value, exponent) computed_value = add_q(computed_value, factor) return computed_value @dataclass class LagrangeCoefficientsRecord: """ Record for lagrange coefficients for specific coordinates, usually the guardian sequence order to be used in the public election record. """ coefficients: Dict[GuardianId, ElementModQ] # pylint: disable=unnecessary-comprehension def compute_lagrange_coefficient(coordinate: int, *degrees: int) -> ElementModQ: """ Compute the lagrange coefficient for a specific coordinate against N degrees. :param coordinate: the coordinate to plot, uisually a Guardian's Sequence Order :param degrees: the degrees across which to plot, usually the collection of available Guardians' Sequence Orders """ numerator = mult_q(*[degree for degree in degrees]) denominator = mult_q(*[(degree - coordinate) for degree in degrees]) result = div_q((numerator), (denominator)) return result def verify_polynomial_coordinate( coordinate: ElementModQ, exponent_modifier: int, commitments: List[PublicCommitment], ) -> bool: """ Verify a polynomial coordinate value is in fact on the polynomial's curve :param coordinate: Value to be checked :param exponent_modifier: Unique modifier (usually sequence order) for exponent :param commitments: Public commitments for coefficients of polynomial :return: True if verified on polynomial """ exponent_modifier_mod_q = ElementModQ(exponent_modifier) commitment_output = ONE_MOD_P for i, commitment in enumerate(commitments): exponent = pow_p(exponent_modifier_mod_q, i) factor = pow_p(commitment, exponent) commitment_output = mult_p(commitment_output, factor) value_output = g_pow_p(coordinate) return value_output == commitment_output ================================================ FILE: src/electionguard/elgamal.py ================================================ from dataclasses import dataclass from typing import Any, Iterable, Optional, Union from .big_integer import bytes_to_hex from .byte_padding import to_padded_bytes from .discrete_log import DiscreteLog from .group import ( ElementModQ, ElementModP, g_pow_p, mult_p, mult_inv_p, pow_p, ZERO_MOD_Q, TWO_MOD_Q, rand_range_q, ) from .hash import hash_elems from .hmac import get_hmac from .logs import log_info, log_error from .utils import get_optional ElGamalSecretKey = ElementModQ ElGamalPublicKey = ElementModP _BLOCK_SIZE = 32 @dataclass class ElGamalKeyPair: """A tuple of an ElGamal secret key and public key.""" secret_key: ElGamalSecretKey public_key: ElGamalPublicKey @dataclass class ElGamalCiphertext: """ An "exponential ElGamal ciphertext" (i.e., with the plaintext in the exponent to allow for homomorphic addition). Create one with `elgamal_encrypt`. Add them with `elgamal_add`. Decrypt using one of the supplied instance methods. """ pad: ElementModP """pad or alpha""" data: ElementModP """encrypted data or beta""" def __eq__(self, other: Any) -> bool: if isinstance(other, ElGamalCiphertext): return self.pad == other.pad and self.data == other.data return False def decrypt_known_product(self, product: ElementModP) -> int: """ Decrypts an ElGamal ciphertext with a "known product" (the blinding factor used in the encryption). :param product: The known product (blinding factor). :return: An exponentially encoded plaintext message. """ return DiscreteLog().discrete_log(mult_p(self.data, mult_inv_p(product))) def decrypt(self, secret_key: ElGamalSecretKey) -> int: """ Decrypt an ElGamal ciphertext using a known ElGamal secret key. :param secret_key: The corresponding ElGamal secret key. :return: An exponentially encoded plaintext message. """ return self.decrypt_known_product(pow_p(self.pad, secret_key)) def decrypt_known_nonce( self, public_key: ElGamalPublicKey, nonce: ElementModQ ) -> int: """ Decrypt an ElGamal ciphertext using a known nonce and the ElGamal public key. :param public_key: The corresponding ElGamal public key. :param nonce: The secret nonce used to create the ciphertext. :return: An exponentially encoded plaintext message. """ return self.decrypt_known_product(pow_p(public_key, nonce)) def partial_decrypt(self, secret_key: ElGamalSecretKey) -> ElementModP: """ Partially Decrypts an ElGamal ciphertext with a known ElGamal secret key. 𝑀_i = 𝐴^𝑠𝑖 mod 𝑝 in the spec :param secret_key: The corresponding ElGamal secret key. :return: An exponentially encoded plaintext message. """ return pow_p(self.pad, secret_key) def crypto_hash(self) -> ElementModQ: """ Computes a cryptographic hash of this ciphertext. """ return hash_elems(self.pad, self.data) @dataclass class HashedElGamalCiphertext: """ A hashed version of ElGamal Ciphertext with less size restrictions. Create one with `hashed_elgamal_encrypt`. Add them with `elgamal_add`. Decrypt using one of the supplied instance methods. """ pad: ElementModP """pad or alpha""" data: str """encrypted data or beta""" mac: str """message authentication code for hmac""" def decrypt( self, secret_key: ElGamalSecretKey, encryption_seed: ElementModQ ) -> Union[bytes, None]: """ Decrypt an ElGamal ciphertext using a known ElGamal secret key. :param secret_key: The corresponding ElGamal secret key. :param encryption_seed: Encryption seed (Q) for election. :return: Decrypted plaintext message. """ session_key = hash_elems(self.pad, pow_p(self.pad, secret_key)) data_bytes = to_padded_bytes(self.data) (ciphertext_chunks, bit_length) = _get_chunks(data_bytes) mac_key = get_hmac( session_key.to_hex_bytes(), encryption_seed.to_hex_bytes(), bit_length, ) to_mac = self.pad.to_hex_bytes() + data_bytes mac = bytes_to_hex(get_hmac(mac_key, to_mac)) if mac != self.mac: log_error("MAC verification failed in decryption.") return None data = b"" for i, block in enumerate(ciphertext_chunks): data_key = get_hmac( session_key.to_hex_bytes(), encryption_seed.to_hex_bytes(), bit_length, (i + 1), ) data += bytes([a ^ b for (a, b) in zip(block, data_key)]) return data def elgamal_keypair_from_secret(a: ElementModQ) -> Optional[ElGamalKeyPair]: """ Given an ElGamal secret key (typically, a random number in [2,Q)), returns an ElGamal keypair, consisting of the given secret key a and public key g^a. """ secret_key_int = a if secret_key_int < 2: log_error("ElGamal secret key needs to be in [2,Q).") return None return ElGamalKeyPair(a, g_pow_p(a)) def elgamal_keypair_random() -> ElGamalKeyPair: """ Create a random elgamal keypair :return: random elgamal key pair """ return get_optional(elgamal_keypair_from_secret(rand_range_q(TWO_MOD_Q))) def elgamal_combine_public_keys(keys: Iterable[ElGamalPublicKey]) -> ElGamalPublicKey: """ Combine multiple elgamal public keys into a joint key :param keys: list of public elgamal keys :return: joint key of elgamal keys """ return mult_p(*keys) def elgamal_encrypt( message: int, nonce: ElementModQ, public_key: ElGamalPublicKey ) -> Optional[ElGamalCiphertext]: """ Encrypts a set length message with a given random nonce and an ElGamal public key. :param message: Known length message (m) to elgamal_encrypt; must be an integer in [0,Q). :param nonce: Randomly chosen nonce in [1,Q). :param public_key: ElGamal public key. :return: An `ElGamalCiphertext`. """ if nonce == ZERO_MOD_Q: log_error("ElGamal encryption requires a non-zero nonce") return None pad = g_pow_p(nonce) gpowp_m = g_pow_p(message) pubkey_pow_n = pow_p(public_key, nonce) data = mult_p(gpowp_m, pubkey_pow_n) log_info(f": publicKey: {public_key.to_hex()}") log_info(f": pad: {pad.to_hex()}") log_info(f": data: {data.to_hex()}") return ElGamalCiphertext(pad, data) def hashed_elgamal_encrypt( message: bytes, nonce: ElementModQ, public_key: ElGamalPublicKey, encryption_seed: ElementModQ, ) -> HashedElGamalCiphertext: """ Encrypts a variable length byte message with a given random nonce and an ElGamal public key. :param message: message (m) to encrypt; must be in bytes. :param nonce: Randomly chosen nonce in [1, Q). :param public_key: ElGamal public key. :param encryption_seed: Encryption seed (Q) for election. """ pad = g_pow_p(nonce) pubkey_pow_n = pow_p(public_key, nonce) session_key = hash_elems(pad, pubkey_pow_n) (message_chunks, bit_length) = _get_chunks(message) data = b"" for i, block in enumerate(message_chunks): data_key = get_hmac( session_key.to_hex_bytes(), encryption_seed.to_hex_bytes(), bit_length, (i + 1), ) data += bytes([a ^ b for (a, b) in zip(block, data_key)]) mac_key = get_hmac( session_key.to_hex_bytes(), encryption_seed.to_hex_bytes(), bit_length ) to_mac = pad.to_hex_bytes() + data mac = get_hmac(mac_key, to_mac) log_info(f": publicKey: {public_key.to_hex()}") log_info(f": pad: {pad.to_hex()}") log_info(f": data: {data!r}") log_info(f": mac: {bytes_to_hex(mac)}") log_info(f"to_mac {to_mac!r}") return HashedElGamalCiphertext(pad, bytes_to_hex(data), bytes_to_hex(mac)) def _get_chunks(message: bytes) -> tuple[list[bytes], int]: remainder = len(message) % _BLOCK_SIZE if remainder: message += bytes([0 for _n in range(_BLOCK_SIZE - remainder)]) number_of_blocks = int(len(message) / _BLOCK_SIZE) return ( [ message[_BLOCK_SIZE * i : _BLOCK_SIZE * (i + 1)] for i in range(number_of_blocks) ], len(message) * 8, ) def elgamal_add(*ciphertexts: ElGamalCiphertext) -> ElGamalCiphertext: """ Homomorphically accumulates one or more ElGamal ciphertexts by pairwise multiplication. The exponents of vote counters will add. """ assert len(ciphertexts) != 0, "Must have one or more ciphertexts for elgamal_add" result = ciphertexts[0] for c in ciphertexts[1:]: result = ElGamalCiphertext( mult_p(result.pad, c.pad), mult_p(result.data, c.data) ) return result ================================================ FILE: src/electionguard/encrypt.py ================================================ from datetime import datetime, timezone from dataclasses import dataclass, field from typing import Dict, List, Optional, Type, TypeVar from uuid import getnode from .ballot import ( CiphertextBallot, CiphertextBallotContest, CiphertextBallotSelection, PlaintextBallot, PlaintextBallotContest, PlaintextBallotSelection, make_ciphertext_ballot_contest, make_ciphertext_ballot_selection, make_ciphertext_ballot, ) from .ballot_code import get_hash_for_device from .election import CiphertextElectionContext from .elgamal import ElGamalPublicKey, elgamal_encrypt, hashed_elgamal_encrypt from .serialize import padded_decode, padded_encode from .group import ElementModQ, rand_q from .logs import log_info, log_warning from .manifest import ( InternalManifest, ContestDescription, ContestDescriptionWithPlaceholders, SelectionDescription, ) from .nonces import Nonces from .type import SelectionId from .utils import ( ContestException, NullVoteException, OverVoteException, UnderVoteException, get_optional, get_or_else_optional_func, ContestErrorType, ) _T = TypeVar("_T", bound="ContestData") @dataclass class ContestData: """Contests errors and extended data from the selections on the contest.""" error: Optional[ContestErrorType] = field(default=None) error_data: Optional[List[SelectionId]] = field(default=None) write_ins: Optional[Dict[SelectionId, str]] = field(default=None) @classmethod def from_bytes(cls: Type[_T], data: bytes) -> _T: return padded_decode(cls, data) def to_bytes(self) -> bytes: return padded_encode(self) @dataclass class EncryptionDevice: """ Metadata for encryption device """ device_id: int """Unique identifier for device""" session_id: int """Used to identify session and protect the timestamp""" launch_code: int """Election initialization value""" location: str """Arbitrary string to designate the location of device""" def get_hash(self) -> ElementModQ: """ Get hash for encryption device :return: Starting hash """ return get_hash_for_device( self.device_id, self.session_id, self.launch_code, self.location ) def get_timestamp(self) -> int: """ Get the current timestamp in utc """ return int(datetime.now(timezone.utc).timestamp()) class EncryptionMediator: """ An object for caching election and encryption state. It composes Elections and Ballots. """ _internal_manifest: InternalManifest _context: CiphertextElectionContext _encryption_seed: ElementModQ def __init__( self, internal_manifest: InternalManifest, context: CiphertextElectionContext, encryption_device: EncryptionDevice, ): self._internal_manifest = internal_manifest self._context = context self._encryption_seed = encryption_device.get_hash() def encrypt(self, ballot: PlaintextBallot) -> Optional[CiphertextBallot]: """ Encrypt the specified ballot using the cached election context. """ log_info(f" encrypt: objectId: {ballot.object_id}") encrypted_ballot = encrypt_ballot( ballot, self._internal_manifest, self._context, self._encryption_seed ) if encrypted_ballot is not None and encrypted_ballot.code is not None: self._encryption_seed = encrypted_ballot.code return encrypted_ballot def generate_device_uuid() -> int: """ Get unique identifier for device :return: Unique identifier """ return getnode() def selection_from( description: SelectionDescription, is_placeholder: bool = False, is_affirmative: bool = False, ) -> PlaintextBallotSelection: """ Construct a `BallotSelection` from a specific `SelectionDescription`. This function is useful for filling selections when a voter undervotes a ballot. It is also used to create placeholder representations when generating the `ConstantChaumPedersenProof` :param description: The `SelectionDescription` which provides the relevant `object_id` :param is_placeholder: Mark this selection as a placeholder value :param is_affirmative: Mark this selection as `yes` :return: A BallotSelection """ return PlaintextBallotSelection( description.object_id, 1 if is_affirmative else 0, is_placeholder, ) def contest_from(description: ContestDescription) -> PlaintextBallotContest: """ Construct a `BallotContest` from a specific `ContestDescription` with all false fields. This function is useful for filling contests and selections when a voter undervotes a ballot. :param description: The `ContestDescription` used to derive the well-formed `BallotContest` :return: a `BallotContest` """ selections: List[PlaintextBallotSelection] = [] for selection_description in description.ballot_selections: selections.append(selection_from(selection_description)) return PlaintextBallotContest(description.object_id, selections) def encrypt_selection( selection: PlaintextBallotSelection, selection_description: SelectionDescription, elgamal_public_key: ElGamalPublicKey, crypto_extended_base_hash: ElementModQ, nonce_seed: ElementModQ, is_placeholder: bool = False, should_verify_proofs: bool = False, ) -> Optional[CiphertextBallotSelection]: """ Encrypt a specific `BallotSelection` in the context of a specific `BallotContest` :param selection: the selection in the valid input form :param selection_description: the `SelectionDescription` from the `ContestDescription` which defines this selection's structure :param elgamal_public_key: the public key (K) used to encrypt the ballot :param crypto_extended_base_hash: the extended base hash of the election :param nonce_seed: an `ElementModQ` used as a header to seed the `Nonce` generated for this selection. this value can be (or derived from) the BallotContest nonce, but no relationship is required :param is_placeholder: specifies if this is a placeholder selection :param should_verify_proofs: specify if the proofs should be verified prior to returning (default False) """ # Validate Input if not selection.is_valid(selection_description.object_id): log_warning(f"malformed input selection: {selection}") return None selection_description_hash = selection_description.crypto_hash() nonce_sequence = Nonces(selection_description_hash, nonce_seed) selection_nonce = nonce_sequence[selection_description.sequence_order] disjunctive_chaum_pedersen_nonce = next(iter(nonce_sequence)) log_info( f": encrypt_selection: for {selection_description.object_id} hash: {selection_description_hash.to_hex()}" ) selection_representation = selection.vote # Generate the encryption elgamal_encryption = elgamal_encrypt( selection_representation, selection_nonce, elgamal_public_key ) if elgamal_encryption is None: # will have logged about the failure earlier, so no need to log anything here return None # TODO: ISSUE #35: encrypt/decrypt: encrypt the extended_data field # Create the return object encrypted_selection = make_ciphertext_ballot_selection( selection.object_id, selection_description.sequence_order, selection_description_hash, get_optional(elgamal_encryption), elgamal_public_key, crypto_extended_base_hash, disjunctive_chaum_pedersen_nonce, selection_representation, is_placeholder, selection_nonce, ) if encrypted_selection.proof is None: return None # log will have happened earlier # optionally, skip the verification step if not should_verify_proofs: return encrypted_selection # verify the selection. if encrypted_selection.is_valid_encryption( selection_description_hash, elgamal_public_key, crypto_extended_base_hash ): return encrypted_selection log_warning( f"mismatching selection proof for selection {encrypted_selection.object_id}" ) return None def encrypt_contest( contest: PlaintextBallotContest, contest_description: ContestDescriptionWithPlaceholders, elgamal_public_key: ElGamalPublicKey, crypto_extended_base_hash: ElementModQ, nonce_seed: ElementModQ, should_verify_proofs: bool = False, ) -> Optional[CiphertextBallotContest]: """ Encrypt a specific `BallotContest` in the context of a specific `Ballot`. This method accepts a contest representation that only includes `True` selections. It will fill missing selections for a contest with `False` values, and generate `placeholder` selections to represent the number of seats available for a given contest. By adding `placeholder` votes :param contest: the contest in the valid input form :param contest_description: the `ContestDescriptionWithPlaceholders` from the `ContestDescription` which defines this contest's structure :param elgamal_public_key: the public key (k) used to encrypt the ballot :param crypto_extended_base_hash: the extended base hash of the election :param nonce_seed: an `ElementModQ` used as a header to seed the `Nonce` generated for this contest. this value can be (or derived from) the Ballot nonce, but no relationship is required :param should_verify_proofs: specify if the proofs should be verified prior to returning (default False) """ error: Optional[ContestErrorType] = None error_data: Optional[List[SelectionId]] = None # Validate Input try: contest.valid(contest_description) except OverVoteException as ove: error = ove.type error_data = ove.overvoted_ids except (NullVoteException, UnderVoteException) as nve: error = nve.type except ContestException as ce: log_warning(str(ce)) return None # account for sequence id contest_description_hash = contest_description.crypto_hash() nonce_sequence = Nonces(contest_description_hash, nonce_seed) contest_nonce = nonce_sequence[contest_description.sequence_order] chaum_pedersen_nonce = next(iter(nonce_sequence)) encrypted_selections: List[CiphertextBallotSelection] = [] selection_count = 0 # TODO: ISSUE #54 this code could be inefficient if we had a contest # with a lot of choices, although the O(n^2) iteration here is small # compared to the huge cost of doing the cryptography. # Generate the encrypted selections for description in contest_description.ballot_selections: has_selection = False encrypted_selection = None # iterate over the actual selections for each contest description # and apply the selected value if it exists. If it does not, an explicit # false is entered instead and the selection_count is not incremented # this allows consumers to only pass in the relevant selections made by a voter for selection in contest.ballot_selections: # If overvote, no votes should be counted and instead placeholders should be used. if ( selection.object_id == description.object_id and error is not ContestErrorType.OverVote ): # track the selection count so we can append the # appropriate number of true placeholder votes has_selection = True selection_count += selection.vote encrypted_selection = encrypt_selection( selection, description, elgamal_public_key, crypto_extended_base_hash, contest_nonce, should_verify_proofs=should_verify_proofs, ) break if not has_selection: # No selection was made for this possible value # so we explicitly set it to false encrypted_selection = encrypt_selection( selection_from(description), description, elgamal_public_key, crypto_extended_base_hash, contest_nonce, should_verify_proofs=should_verify_proofs, ) if encrypted_selection is None: return None # log will have happened earlier encrypted_selections.append(get_optional(encrypted_selection)) # Handle Placeholder selections # After we loop through all of the real selections on the ballot, # we loop through each placeholder value and determine if it should be filled in # Add a placeholder selection for each possible seat in the contest for placeholder in contest_description.placeholder_selections: # for undervotes, select the placeholder value as true for each available seat # note this pattern is used since DisjunctiveChaumPedersen expects a 0 or 1 # so each seat can only have a maximum value of 1 in the current implementation select_placeholder = False if selection_count < contest_description.number_elected: select_placeholder = True selection_count += 1 encrypted_selection = encrypt_selection( selection=selection_from( description=placeholder, is_placeholder=True, is_affirmative=select_placeholder, ), selection_description=placeholder, elgamal_public_key=elgamal_public_key, crypto_extended_base_hash=crypto_extended_base_hash, nonce_seed=contest_nonce, is_placeholder=True, should_verify_proofs=should_verify_proofs, ) if encrypted_selection is None: return None # log will have happened earlier encrypted_selections.append(get_optional(encrypted_selection)) encrypted_contest_data = hashed_elgamal_encrypt( ContestData(error, error_data, contest.write_ins).to_bytes(), Nonces(contest_nonce, "constant-extended-data")[0], elgamal_public_key, crypto_extended_base_hash, ) # Create the return object encrypted_contest = make_ciphertext_ballot_contest( contest.object_id, contest_description.sequence_order, contest_description_hash, encrypted_selections, elgamal_public_key, crypto_extended_base_hash, chaum_pedersen_nonce, contest_description.number_elected, nonce=contest_nonce, extended_data=encrypted_contest_data, ) if should_verify_proofs or not encrypted_contest.proof: if encrypted_contest.is_valid_encryption( contest_description_hash, elgamal_public_key, crypto_extended_base_hash ): return encrypted_contest log_warning( f"mismatching contest proof for contest {encrypted_contest.object_id}" ) return None return encrypted_contest # TODO: ISSUE #57: add the device hash to the function interface so it can be propagated with the ballot. # also propagate the seed so that the ballot codes can be regenerated # by traversing the collection of ballots encrypted by a specific device def encrypt_ballot( ballot: PlaintextBallot, internal_manifest: InternalManifest, context: CiphertextElectionContext, encryption_seed: ElementModQ, nonce: Optional[ElementModQ] = None, should_verify_proofs: bool = False, ) -> Optional[CiphertextBallot]: """ Encrypt a specific `Ballot` in the context of a specific `CiphertextElectionContext`. This method accepts a ballot representation that only includes `True` selections. It will fill missing selections for a contest with `False` values, and generate `placeholder` selections to represent the number of seats available for a given contest. This method also allows for ballots to exclude passing contests for which the voter made no selections. It will fill missing contests with `False` selections and generate `placeholder` selections that are marked `True`. :param ballot: the ballot in the valid input form :param internal_manifest: the `InternalManifest` which defines this ballot's structure :param context: all the cryptographic context for the election :param encryption_seed: Hash from previous ballot or starting hash from device :param nonce: an optional `int` used to seed the `Nonce` generated for this contest if this value is not provided, the secret generating mechanism of the OS provides its own :param should_verify_proofs: specify if the proofs should be verified prior to returning (default False) """ # Determine the relevant range of contests for this ballot style style = internal_manifest.get_ballot_style(ballot.style_id) # Validate Input if not ballot.is_valid(style.object_id): log_warning(f"malformed input ballot: {ballot}") return None # Generate a random master nonce to use for the contest and selection nonce's on the ballot random_master_nonce = get_or_else_optional_func(nonce, lambda: rand_q()) # Include a representation of the election and the external Id in the nonce's used # to derive other nonce values on the ballot nonce_seed = CiphertextBallot.nonce_seed( internal_manifest.manifest_hash, ballot.object_id, random_master_nonce, ) log_info(f": manifest_hash : {internal_manifest.manifest_hash.to_hex()}") log_info(f": encryption_seed : {encryption_seed.to_hex()}") encrypted_contests = encrypt_ballot_contests( ballot, internal_manifest, context, nonce_seed, should_verify_proofs=should_verify_proofs, ) if encrypted_contests is None: return None # Create the return object encrypted_ballot = make_ciphertext_ballot( ballot.object_id, ballot.style_id, internal_manifest.manifest_hash, encryption_seed, encrypted_contests, random_master_nonce, ) if not encrypted_ballot.code: return None if not should_verify_proofs: return encrypted_ballot # Verify the proofs if encrypted_ballot.is_valid_encryption( internal_manifest.manifest_hash, context.elgamal_public_key, context.crypto_extended_base_hash, ): return encrypted_ballot return None # log will have happened earlier def encrypt_ballot_contests( ballot: PlaintextBallot, description: InternalManifest, context: CiphertextElectionContext, nonce_seed: ElementModQ, should_verify_proofs: bool = False, ) -> Optional[List[CiphertextBallotContest]]: """Encrypt contests from a plaintext ballot with a specific style""" encrypted_contests: List[CiphertextBallotContest] = [] # Only iterate on contests for this specific ballot style for ballot_style_contest in description.get_contests_for(ballot.style_id): use_contest = None for contest in ballot.contests: if contest.object_id == ballot_style_contest.object_id: use_contest = contest break # no selections provided for the contest, so create a placeholder contest if not use_contest: use_contest = contest_from(ballot_style_contest) encrypted_contest = encrypt_contest( use_contest, ballot_style_contest, context.elgamal_public_key, context.crypto_extended_base_hash, nonce_seed, should_verify_proofs=should_verify_proofs, ) if encrypted_contest is None: return None encrypted_contests.append(get_optional(encrypted_contest)) return encrypted_contests ================================================ FILE: src/electionguard/group.py ================================================ """Basic modular math module. Support for basic modular math in ElectionGuard. This code's primary purpose is to be "correct", in the sense that performance may be less than hand-optimized C code, and no guarantees are made about timing or other side-channels. """ from abc import ABC from typing import Final, Optional, Union from secrets import randbelow from sys import maxsize # pylint: disable=no-name-in-module from gmpy2 import mpz, powmod, invert from .big_integer import BigInteger from .constants import get_large_prime, get_small_prime, get_generator class BaseElement(BigInteger, ABC): """An element limited by mod T within [0, T) where T is determined by an upper_bound function.""" def __new__(cls, data: Union[int, str], check_within_bounds: bool = True): # type: ignore """Instantiate element mod T where element is an int or its hex representation.""" element = super(BaseElement, cls).__new__(cls, data) if check_within_bounds: if not 0 <= element.value < cls.get_upper_bound(): raise OverflowError return element @classmethod def get_upper_bound(cls) -> int: """Get the upper bound for the element.""" return maxsize def is_in_bounds(self) -> bool: """ Validate that the element is actually within the bounds of [0,Q). Returns true if all is good, false if something's wrong. """ return 0 <= self.value < self.get_upper_bound() def is_in_bounds_no_zero(self) -> bool: """ Validate that the element is actually within the bounds of [1,Q). Returns true if all is good, false if something's wrong. """ return 1 <= self.value < self.get_upper_bound() class ElementModQ(BaseElement): """An element of the smaller `mod q` space, i.e., in [0, Q), where Q is a 256-bit prime.""" @classmethod def get_upper_bound(cls) -> int: """Get the upper bound for the element.""" return get_small_prime() class ElementModP(BaseElement): """An element of the larger `mod p` space, i.e., in [0, P), where P is a 4096-bit prime.""" @classmethod def get_upper_bound(cls) -> int: """Get the upper bound for the element.""" return get_large_prime() def is_valid_residue(self) -> bool: """Validate that this element is in Z^r_p.""" residue = pow_p(self, get_small_prime()) == ONE_MOD_P return self.is_in_bounds() and residue # Common constants ZERO_MOD_Q: Final[ElementModQ] = ElementModQ(0) ONE_MOD_Q: Final[ElementModQ] = ElementModQ(1) TWO_MOD_Q: Final[ElementModQ] = ElementModQ(2) ZERO_MOD_P: Final[ElementModP] = ElementModP(0) ONE_MOD_P: Final[ElementModP] = ElementModP(1) TWO_MOD_P: Final[ElementModP] = ElementModP(2) ElementModPOrQ = Union[ElementModP, ElementModQ] ElementModPOrQorInt = Union[ElementModP, ElementModQ, int] ElementModQorInt = Union[ElementModQ, int] ElementModPorInt = Union[ElementModP, int] def _get_mpz(input: Union[BaseElement, int]) -> mpz: """Get BaseElement or integer as mpz.""" if isinstance(input, BaseElement): return input.value return mpz(input) def hex_to_q(input: str) -> Optional[ElementModQ]: """ Given a hex string representing bytes, returns an ElementModQ. Returns `None` if the number is out of the allowed [0,Q) range. """ try: return ElementModQ(input) except OverflowError: return None def int_to_q(input: int) -> Optional[ElementModQ]: """ Given a Python integer, returns an ElementModQ. Returns `None` if the number is out of the allowed [0,Q) range. """ try: return ElementModQ(input) except OverflowError: return None def hex_to_p(input: str) -> Optional[ElementModP]: """ Given a hex string representing bytes, returns an ElementModP. Returns `None` if the number is out of the allowed [0,Q) range. """ try: return ElementModP(input) except OverflowError: return None def int_to_p(input: int) -> Optional[ElementModP]: """ Given a Python integer, returns an ElementModP. Returns `None` if the number is out of the allowed [0,P) range. """ try: return ElementModP(input) except OverflowError: return None def add_q(*elems: ElementModQorInt) -> ElementModQ: """Add together one or more elements in Q, returns the sum mod Q.""" sum = _get_mpz(0) for e in elems: e = _get_mpz(e) sum = (sum + e) % get_small_prime() return ElementModQ(sum) def a_minus_b_q(a: ElementModQorInt, b: ElementModQorInt) -> ElementModQ: """Compute (a-b) mod q.""" a = _get_mpz(a) b = _get_mpz(b) return ElementModQ((a - b) % get_small_prime()) def div_p(a: ElementModPOrQorInt, b: ElementModPOrQorInt) -> ElementModP: """Compute a/b mod p.""" b = _get_mpz(b) inverse = invert(b, _get_mpz(get_large_prime())) return mult_p(a, inverse) def div_q(a: ElementModPOrQorInt, b: ElementModPOrQorInt) -> ElementModQ: """Compute a/b mod q.""" b = _get_mpz(b) inverse = invert(b, _get_mpz(get_small_prime())) return mult_q(a, inverse) def negate_q(a: ElementModQorInt) -> ElementModQ: """Compute (Q - a) mod q.""" a = _get_mpz(a) return ElementModQ(get_small_prime() - a) def a_plus_bc_q( a: ElementModQorInt, b: ElementModQorInt, c: ElementModQorInt ) -> ElementModQ: """Compute (a + b * c) mod q.""" a = _get_mpz(a) b = _get_mpz(b) c = _get_mpz(c) return ElementModQ((a + b * c) % get_small_prime()) def mult_inv_p(e: ElementModPOrQorInt) -> ElementModP: """ Compute the multiplicative inverse mod p. :param e: An element in [1, P). """ e = _get_mpz(e) assert e != 0, "No multiplicative inverse for zero" return ElementModP(powmod(e, -1, get_large_prime())) def pow_p(b: ElementModPOrQorInt, e: ElementModPOrQorInt) -> ElementModP: """ Compute b^e mod p. :param b: An element in [0,P). :param e: An element in [0,P). """ b = _get_mpz(b) e = _get_mpz(e) return ElementModP(powmod(b, e, get_large_prime())) def pow_q(b: ElementModQorInt, e: ElementModQorInt) -> ElementModQ: """ Compute b^e mod q. :param b: An element in [0,Q). :param e: An element in [0,Q). """ b = _get_mpz(b) e = _get_mpz(e) return ElementModQ(powmod(b, e, get_small_prime())) def mult_p(*elems: ElementModPOrQorInt) -> ElementModP: """ Compute the product, mod p, of all elements. :param elems: Zero or more elements in [0,P). """ product = _get_mpz(1) for x in elems: x = _get_mpz(x) product = (product * x) % get_large_prime() return ElementModP(product) def mult_q(*elems: ElementModPOrQorInt) -> ElementModQ: """ Compute the product, mod q, of all elements. :param elems: Zero or more elements in [0,Q). """ product = _get_mpz(1) for x in elems: x = _get_mpz(x) product = (product * x) % get_small_prime() return ElementModQ(product) def g_pow_p(e: ElementModPOrQorInt) -> ElementModP: """ Compute g^e mod p. :param e: An element in [0,P). """ return pow_p(get_generator(), e) def rand_q() -> ElementModQ: """ Generate random number between 0 and Q. :return: Random value between 0 and Q """ return ElementModQ(randbelow(get_small_prime())) def rand_range_q(start: ElementModQorInt) -> ElementModQ: """ Generate random number between start and Q. :param start: Starting value of range :return: Random value between start and Q """ start = _get_mpz(start) random = 0 while random < start: random = randbelow(get_small_prime()) return ElementModQ(random) ================================================ FILE: src/electionguard/guardian.py ================================================ # pylint: disable=too-many-public-methods from dataclasses import dataclass from typing import Dict, List, Optional, TypeVar from electionguard.utils import get_optional from .ballot import SubmittedBallot from .decryption import ( compute_compensated_decryption_share, compute_compensated_decryption_share_for_ballot, compute_decryption_share, compute_decryption_share_for_ballot, decrypt_backup, ) from .decryption_share import CompensatedDecryptionShare, DecryptionShare from .election import CiphertextElectionContext from .election_polynomial import ElectionPolynomial, PublicCommitment from .elgamal import ElGamalKeyPair, ElGamalPublicKey, elgamal_combine_public_keys from .group import ElementModP, ElementModQ from .key_ceremony import ( CeremonyDetails, ElectionKeyPair, ElectionPartialKeyBackup, ElectionPartialKeyChallenge, ElectionPartialKeyVerification, ElectionPublicKey, generate_election_key_pair, generate_election_partial_key_backup, generate_election_partial_key_challenge, verify_election_partial_key_backup, verify_election_partial_key_challenge, ) from .logs import log_warning from .schnorr import SchnorrProof from .tally import CiphertextTally from .type import BallotId, GuardianId @dataclass class GuardianRecord: """ Published record containing all required information per Guardian for Election record used in verification processes """ guardian_id: GuardianId """Unique identifier of the guardian""" sequence_order: int """ Unique sequence order of the guardian indicating the order in which the guardian should be processed """ election_public_key: ElGamalPublicKey """ Guardian's election public key for encrypting election objects. """ election_commitments: List[PublicCommitment] """ Commitment for each coeffficient of the guardians secret polynomial. First commitment is and should be identical to election_public_key. """ election_proofs: List[SchnorrProof] """ Proofs for each commitment for each coeffficient of the guardians secret polynomial. First proof is the proof for the election_public_key. """ def publish_guardian_record(election_public_key: ElectionPublicKey) -> GuardianRecord: """ Published record containing all required information per Guardian for Election record used in verification processes :param election_public_key: Guardian's election public key :return: Guardian's record """ return GuardianRecord( election_public_key.owner_id, election_public_key.sequence_order, election_public_key.key, election_public_key.coefficient_commitments, election_public_key.coefficient_proofs, ) @dataclass class PrivateGuardianRecord: """Unpublishable private record containing information per Guardian.""" guardian_id: GuardianId """Unique identifier of the guardian""" election_keys: ElectionKeyPair """Private election Key pair of this guardian""" backups_to_share: Dict[GuardianId, ElectionPartialKeyBackup] """This guardian's partial key backups that will be shared to other guardians""" guardian_election_public_keys: Dict[GuardianId, ElectionPublicKey] """Received election public keys that are shared with this guardian""" guardian_election_partial_key_backups: Dict[GuardianId, ElectionPartialKeyBackup] """Received partial key backups that are shared with this guardian""" guardian_election_partial_key_verifications: Dict[ GuardianId, ElectionPartialKeyVerification ] """Verifications of other guardian's backups""" class Guardian: """ Guardian of election responsible for safeguarding information and decrypting results. The first half of the guardian involves the key exchange known as the key ceremony. The second half relates to the decryption process. """ _election_keys: ElectionKeyPair ceremony_details: CeremonyDetails _backups_to_share: Dict[GuardianId, ElectionPartialKeyBackup] """ The collection of this guardian's partial key backups that will be shared to other guardians """ # From Other Guardians _guardian_election_public_keys: Dict[GuardianId, ElectionPublicKey] """ The collection of other guardians' election public keys that are shared with this guardian """ _guardian_election_partial_key_backups: Dict[GuardianId, ElectionPartialKeyBackup] """ The collection of other guardians' partial key backups that are shared with this guardian """ _guardian_election_partial_key_verifications: Dict[ GuardianId, ElectionPartialKeyVerification ] """ The collection of other guardians' verifications that they shared their backups correctly """ def __init__( self, key_pair: ElectionKeyPair, ceremony_details: CeremonyDetails, election_public_keys: Optional[Dict[GuardianId, ElectionPublicKey]] = None, partial_key_backups: Optional[ Dict[GuardianId, ElectionPartialKeyBackup] ] = None, backups_to_share: Optional[Dict[GuardianId, ElectionPartialKeyBackup]] = None, guardian_election_partial_key_verifications: Optional[ Dict[GuardianId, ElectionPartialKeyVerification] ] = None, ) -> None: """ Initialize a guardian with the specified arguments. :param key_pair The key pair the guardian generated during a key ceremony :param ceremony_details The details of the key ceremony :param election_public_keys the public keys the guardian generated during a key ceremony :param partial_key_backups the partial key backups the guardian generated during a key ceremony """ self._election_keys = key_pair self.ceremony_details = ceremony_details # Reduce this ⬇️ self._backups_to_share = {} if backups_to_share is None else backups_to_share self._guardian_election_public_keys = ( {} if election_public_keys is None else election_public_keys ) self._guardian_election_partial_key_backups = ( {} if partial_key_backups is None else partial_key_backups ) self._guardian_election_partial_key_verifications = ( {} if guardian_election_partial_key_verifications is None else guardian_election_partial_key_verifications ) self.save_guardian_key(key_pair.share()) @property def id(self) -> GuardianId: return self._election_keys.owner_id @property def sequence_order(self) -> int: return self._election_keys.sequence_order @classmethod def from_public_key( cls, number_of_guardians: int, quorum: int, public_key: ElectionPublicKey, ) -> "Guardian": el_gamal_key_pair = ElGamalKeyPair(ElementModQ(0), public_key.key) election_key_pair = ElectionKeyPair( public_key.owner_id, public_key.sequence_order, el_gamal_key_pair, ElectionPolynomial([]), ) ceremony_details = CeremonyDetails(number_of_guardians, quorum) return cls(election_key_pair, ceremony_details) @classmethod def from_nonce( cls, id: str, sequence_order: int, number_of_guardians: int, quorum: int, nonce: Optional[ElementModQ] = None, ) -> "Guardian": """Creates a guardian with an `ElementModQ` value that will be used to generate the `ElectionKeyPair`. If no nonce provided, this will be generated automatically. This method should generally only be used for testing.""" key_pair = generate_election_key_pair(id, sequence_order, quorum, nonce) ceremony_details = CeremonyDetails(number_of_guardians, quorum) return cls(key_pair, ceremony_details) @classmethod def from_private_record( cls, private_guardian_record: PrivateGuardianRecord, number_of_guardians: int, quorum: int, ) -> "Guardian": guardian = cls( private_guardian_record.election_keys, CeremonyDetails(number_of_guardians, quorum), private_guardian_record.guardian_election_public_keys, private_guardian_record.guardian_election_partial_key_backups, private_guardian_record.backups_to_share, private_guardian_record.guardian_election_partial_key_verifications, ) return guardian def publish(self) -> GuardianRecord: """Publish record of guardian with all required information.""" return publish_guardian_record(self._election_keys.share()) def export_private_data(self) -> PrivateGuardianRecord: """Export private data of guardian. Warning cannot be published.""" return PrivateGuardianRecord( self.id, self._election_keys, self._backups_to_share, self._guardian_election_public_keys, self._guardian_election_partial_key_backups, self._guardian_election_partial_key_verifications, ) def set_ceremony_details(self, number_of_guardians: int, quorum: int) -> None: """ Set ceremony details for election. :param number_of_guardians: Number of guardians in election :param quorum: Quorum of guardians required to decrypt """ self.ceremony_details = CeremonyDetails(number_of_guardians, quorum) def decrypt_backup(self, backup: ElectionPartialKeyBackup) -> Optional[ElementModQ]: """ Decrypts a compensated partial decryption of an elgamal encryption on behalf of a missing guardian. :param backup: An encrypted backup from a missing guardian. :return: A decrypted backup. """ return decrypt_backup(get_optional(backup), self._election_keys) # Public Keys def share_key(self) -> ElectionPublicKey: """ Share election public key with another guardian. :return: Election public key """ return self._election_keys.share() def save_guardian_key(self, key: ElectionPublicKey) -> None: """ Save public election keys for another guardian. :param key: Election public key """ self._guardian_election_public_keys[key.owner_id] = key def all_guardian_keys_received(self) -> bool: """ True if all keys have been received. :return: All keys backups received """ return ( len(self._guardian_election_public_keys) == self.ceremony_details.number_of_guardians ) def generate_election_partial_key_backups(self) -> bool: """ Generate all election partial key backups based on existing public keys. """ for guardian_key in self._guardian_election_public_keys.values(): backup = generate_election_partial_key_backup( self.id, self._election_keys.polynomial, guardian_key ) if backup is None: log_warning( f"guardian; {self.id} could not generate election partial key backups: failed to encrypt" ) return False self._backups_to_share[guardian_key.owner_id] = backup return True # Election Partial Key Backup def share_election_partial_key_backup( self, designated_id: GuardianId ) -> Optional[ElectionPartialKeyBackup]: """ Share election partial key backup with another guardian. :param designated_id: Designated guardian :return: Election partial key backup or None """ return self._backups_to_share.get(designated_id) def share_election_partial_key_backups(self) -> List[ElectionPartialKeyBackup]: """ Share all election partial key backups. :return: Election partial key backup or None """ return list(self._backups_to_share.values()) def save_election_partial_key_backup( self, backup: ElectionPartialKeyBackup ) -> None: """ Save election partial key backup from another guardian. :param backup: Election partial key backup """ self._guardian_election_partial_key_backups[backup.owner_id] = backup def all_election_partial_key_backups_received(self) -> bool: """ True if all election partial key backups have been received. :return: All election partial key backups received """ return ( len(self._guardian_election_partial_key_backups) == self.ceremony_details.number_of_guardians - 1 ) # Verification def verify_election_partial_key_backup( self, guardian_id: GuardianId, ) -> Optional[ElectionPartialKeyVerification]: """ Verify election partial key backup value is in polynomial. :param guardian_id: Owner of backup to verify :param decrypt: :return: Election partial key verification or None """ backup = self._guardian_election_partial_key_backups.get(guardian_id) public_key = self._guardian_election_public_keys.get(guardian_id) if backup is None: raise ValueError(f"No backup exists for {guardian_id}") if public_key is None: raise ValueError(f"No public key exists for {guardian_id}") return verify_election_partial_key_backup( self.id, backup, public_key, self._election_keys ) def publish_election_backup_challenge( self, guardian_id: GuardianId ) -> Optional[ElectionPartialKeyChallenge]: """ Publish election backup challenge of election partial key verification. :param guardian_id: Owner of election key :return: Election partial key challenge or None """ backup_in_question = self._backups_to_share.get(guardian_id) if backup_in_question is None: return None return generate_election_partial_key_challenge( backup_in_question, self._election_keys.polynomial ) def verify_election_partial_key_challenge( self, challenge: ElectionPartialKeyChallenge ) -> ElectionPartialKeyVerification: """ Verify challenge of previous verification of election partial key. :param challenge: Election partial key challenge :return: Election partial key verification """ return verify_election_partial_key_challenge(self.id, challenge) def save_election_partial_key_verification( self, verification: ElectionPartialKeyVerification ) -> None: """ Save election partial key verification from another guardian. :param verification: Election partial key verification """ self._guardian_election_partial_key_verifications[ verification.designated_id ] = verification def all_election_partial_key_backups_verified(self) -> bool: """ True if all election partial key backups have been verified. :return: All election partial key backups verified """ required = self.ceremony_details.number_of_guardians - 1 if len(self._guardian_election_partial_key_verifications) != required: return False for verification in self._guardian_election_partial_key_verifications.values(): if not verification.verified: return False return True # Joint Key def publish_joint_key(self) -> Optional[ElementModP]: """ Create the joint election key from the public keys of all guardians. :return: Optional joint key for election """ if not self.all_guardian_keys_received(): return None if not self.all_election_partial_key_backups_verified(): return None public_keys = map( lambda public_key: public_key.key, self._guardian_election_public_keys.values(), ) return elgamal_combine_public_keys(public_keys) def share_other_guardian_key( self, guardian_id: GuardianId ) -> Optional[ElectionPublicKey]: """Share other guardians keys shared during key ceremony""" return self._guardian_election_public_keys.get(guardian_id) def compute_tally_share( self, tally: CiphertextTally, context: CiphertextElectionContext ) -> Optional[DecryptionShare]: """ Compute the decryption share of tally. :param tally: Ciphertext tally to get share of :param context: Election context :return: Decryption share of tally or None if failure """ return compute_decryption_share( self._election_keys, tally, context, ) def compute_ballot_shares( self, ballots: List[SubmittedBallot], context: CiphertextElectionContext ) -> Dict[BallotId, Optional[DecryptionShare]]: """ Compute the decryption shares of ballots. :param ballots: List of ciphertext ballots to get shares of :param context: Election context :return: Decryption shares of ballots or None if failure """ shares = {} for ballot in ballots: share = compute_decryption_share_for_ballot( self._election_keys, ballot, context, ) shares[ballot.object_id] = share return shares def compute_compensated_tally_share( self, missing_guardian_id: GuardianId, tally: CiphertextTally, context: CiphertextElectionContext, ) -> Optional[CompensatedDecryptionShare]: """ Compute the compensated decryption share of a tally for a missing guardian. :param missing_guardian_id: Missing guardians id :param tally: Ciphertext tally to get share of :param context: Election context :return: Compensated decryption share of tally or None if failure """ # Ensure missing guardian information available missing_guardian_key = self._guardian_election_public_keys.get( missing_guardian_id ) missing_guardian_backup = self._guardian_election_partial_key_backups.get( missing_guardian_id ) if missing_guardian_key is None or missing_guardian_backup is None: return None missing_guardian_coordinate = self.decrypt_backup(missing_guardian_backup) return compute_compensated_decryption_share( get_optional(missing_guardian_coordinate), self.share_key(), missing_guardian_key, tally, context, ) def compute_compensated_ballot_shares( self, missing_guardian_id: GuardianId, ballots: List[SubmittedBallot], context: CiphertextElectionContext, ) -> Dict[BallotId, Optional[CompensatedDecryptionShare]]: """ Compute the compensated decryption share of each ballots for a missing guardian. :param missing_guardian_id: Missing guardians id :param ballots: List of ciphertext ballots to get shares of :param context: Election context :return: Compensated decryption shares of ballots or None if failure """ shares: Dict[BallotId, Optional[CompensatedDecryptionShare]] = { ballot.object_id: None for ballot in ballots } # Ensure missing guardian information available missing_guardian_key = self._guardian_election_public_keys.get( missing_guardian_id ) missing_guardian_backup = self._guardian_election_partial_key_backups.get( missing_guardian_id ) if missing_guardian_key is None or missing_guardian_backup is None: return shares missing_guardian_coordinate = self.decrypt_backup(missing_guardian_backup) for ballot in ballots: share = compute_compensated_decryption_share_for_ballot( get_optional(missing_guardian_coordinate), missing_guardian_key, self.share_key(), ballot, context, ) shares[ballot.object_id] = share return shares _SHARE = TypeVar("_SHARE") def get_valid_ballot_shares( ballot_shares: Dict[BallotId, Optional[_SHARE]], ) -> Dict[BallotId, _SHARE]: """Get valid ballot shares.""" filtered_shares = {} for ballot_id, ballot_share in ballot_shares.items(): if ballot_share is not None: filtered_shares[ballot_id] = ballot_share return filtered_shares ================================================ FILE: src/electionguard/hash.py ================================================ # pylint: disable=isinstance-second-argument-not-valid-type from abc import abstractmethod from collections.abc import Sequence from hashlib import sha256 from typing import ( Iterable, List, Union, Protocol, runtime_checkable, Sequence as TypedSequence, ) from .constants import get_small_prime from .utils import BYTE_ENCODING, BYTE_ORDER from .group import ( ElementModPOrQ, ElementModQ, ElementModP, ) @runtime_checkable class CryptoHashable(Protocol): """ Denotes hashable """ @abstractmethod def crypto_hash(self) -> ElementModQ: """ Generates a hash given the fields on the implementing instance. """ @runtime_checkable class CryptoHashCheckable(Protocol): """ Checkable version of crypto hash """ @abstractmethod def crypto_hash_with(self, encryption_seed: ElementModQ) -> ElementModQ: """ Generates a hash with a given seed that can be checked later against the seed and class metadata. """ # All the "atomic" types that we know how to hash. CryptoHashableT = Union[CryptoHashable, ElementModPOrQ, str, int, None] # "Compound" types that we know how to hash. Note that we're using Sequence, rather than List, # because Sequences are read-only, and thus safely covariant. All this really means is that # we promise never to mutate any list that you pass to hash_elems. CryptoHashableAll = Union[ TypedSequence[CryptoHashableT], CryptoHashableT, ] def hash_elems(*a: CryptoHashableAll) -> ElementModQ: """ Given zero or more elements, calculate their cryptographic hash using SHA256. Allowed element types are `ElementModP`, `ElementModQ`, `str`, or `int`, anything implementing `CryptoHashable`, and lists or optionals of any of those types. :param a: Zero or more elements of any of the accepted types. :return: A cryptographic hash of these elements, concatenated. """ h = sha256() h.update("|".encode(BYTE_ENCODING)) for x in a: # We could just use str(x) for everything, but then we'd have a resulting string # that's a bit Python-specific, and we'd rather make it easier for other languages # to exactly match this hash function. if isinstance(x, (ElementModP, ElementModQ)): hash_me = x.to_hex() elif isinstance(x, CryptoHashable): hash_me = x.crypto_hash().to_hex() elif isinstance(x, str): # strings are iterable, so it's important to handle them before list-like types hash_me = x elif isinstance(x, int): hash_me = str(x) elif not x: # This case captures empty lists and None, nicely guaranteeing that we don't # need to do a recursive call if the list is empty. So we need a string to # feed in for both of these cases. "None" would be a Python-specific thing, # so we'll go with the more JSON-ish "null". hash_me = "null" elif isinstance(x, (Sequence, List, Iterable)): # The simplest way to deal with lists, tuples, and such are to crunch them recursively. hash_me = hash_elems(*x).to_hex() else: hash_me = str(x) h.update((hash_me + "|").encode(BYTE_ENCODING)) return ElementModQ( int.from_bytes(h.digest(), byteorder=BYTE_ORDER) % get_small_prime() ) ================================================ FILE: src/electionguard/hmac.py ================================================ """Implementation of Hashing for Message Authentication Codes (HMAC)""" from hmac import digest from typing import Optional, Literal _BYTE_LENGTH = 4 _BYTE_ORDER: Literal["little", "big"] = "little" def get_hmac( key: bytes, message: bytes, length: Optional[int] = None, start: int = 0 ) -> bytes: """ Get a hash-based message authentication code(hmac) digest using default hashing algorithm. :param key: key (k) in bytes :param message: message in bytes :param length: length (L) of total message :param start: starting byte position :return: hmac digest in bytes """ if length: message = _fix_message_length(message, length, start) return digest(key, message, "SHA256") def _fix_message_length(msg: bytes, length: int, start: int = 0) -> bytes: """ Fix the message length to a set byte length with starting and end bytes. :param msg: message :param length: length (L) :param start: start of byte """ start_byte = start.to_bytes(_BYTE_LENGTH, _BYTE_ORDER) end_byte = length.to_bytes(_BYTE_LENGTH, _BYTE_ORDER) return start_byte + msg + end_byte ================================================ FILE: src/electionguard/key_ceremony.py ================================================ from dataclasses import dataclass from typing import List, Type, TypeVar, Optional from .serialize import padded_decode, padded_encode from .election_polynomial import ( PublicCommitment, compute_polynomial_coordinate, ElectionPolynomial, generate_polynomial, verify_polynomial_coordinate, ) from .elgamal import ( ElGamalKeyPair, ElGamalPublicKey, HashedElGamalCiphertext, elgamal_combine_public_keys, hashed_elgamal_encrypt, ) from .group import ElementModQ, rand_q from .hash import hash_elems from .schnorr import SchnorrProof from .type import ( GuardianId, VerifierId, ) from .utils import get_optional @dataclass class ElectionPublicKey: """A tuple of election public key and owner information""" owner_id: GuardianId """ The id of the owner guardian """ sequence_order: int """ The sequence order of the owner guardian """ key: ElGamalPublicKey """ The election public for the guardian Note: This is the same as the first coefficient commitment """ coefficient_commitments: List[PublicCommitment] """ The commitments for the coefficients in the secret polynomial """ coefficient_proofs: List[SchnorrProof] """ The proofs for the coefficients in the secret polynomial """ @dataclass class ElectionKeyPair: """A tuple of election key pair, proof and polynomial""" owner_id: GuardianId """ The id of the owner guardian """ sequence_order: int """ The sequence order of the owner guardian """ key_pair: ElGamalKeyPair """ The pair of public and private election keys for the guardian """ polynomial: ElectionPolynomial """ The secret polynomial for the guardian """ def share(self) -> ElectionPublicKey: """Share the election public key and associated data""" return ElectionPublicKey( self.owner_id, self.sequence_order, self.key_pair.public_key, self.polynomial.get_commitments(), self.polynomial.get_proofs(), ) @dataclass class ElectionJointKey: """ The Election joint key """ joint_public_key: ElGamalPublicKey """ The product of the guardian public keys K = ∏ ni=1 Ki mod p. """ commitment_hash: ElementModQ """ The hash of the commitments that the guardians make to each other H = H(K 1,0 , K 2,0 ... , K n,0 ) """ @dataclass class ElectionPartialKeyBackup: """Election partial key backup used for key sharing""" owner_id: GuardianId """ The Id of the guardian that generated this backup """ designated_id: GuardianId """ The Id of the guardian to receive this backup """ designated_sequence_order: int """ The sequence order of the designated guardian """ encrypted_coordinate: HashedElGamalCiphertext """ The coordinate corresponding to a secret election polynomial """ @dataclass class CeremonyDetails: """Details of key ceremony""" number_of_guardians: int quorum: int @dataclass class ElectionPartialKeyVerification: """Verification of election partial key used in key sharing""" owner_id: GuardianId designated_id: GuardianId verifier_id: GuardianId verified: bool @dataclass class ElectionPartialKeyChallenge: """Challenge of election partial key used in key sharing""" owner_id: GuardianId designated_id: GuardianId designated_sequence_order: int value: ElementModQ coefficient_commitments: List[PublicCommitment] coefficient_proofs: List[SchnorrProof] _T = TypeVar("_T", bound="CoordinateData") @dataclass class CoordinateData: """A coordinate from a PartialKeyBackup that can be serialized and deserialized for encryption/decryption""" coordinate: ElementModQ @classmethod def from_bytes(cls: Type[_T], data: bytes) -> _T: return padded_decode(cls, data) def to_bytes(self) -> bytes: return padded_encode(self) def generate_election_key_pair( guardian_id: str, sequence_order: int, quorum: int, nonce: Optional[ElementModQ] = None, ) -> ElectionKeyPair: """ Generate election key pair, proof, and polynomial :param quorum: Quorum of guardians needed to decrypt :return: Election key pair """ polynomial = generate_polynomial(quorum, nonce) key_pair = ElGamalKeyPair( polynomial.coefficients[0].value, polynomial.coefficients[0].commitment ) return ElectionKeyPair(guardian_id, sequence_order, key_pair, polynomial) def generate_election_partial_key_backup( sender_guardian_id: GuardianId, sender_guardian_polynomial: ElectionPolynomial, receiver_guardian_public_key: ElectionPublicKey, ) -> ElectionPartialKeyBackup: """ Generate election partial key backup for sharing :param sender_guardian_id: Owner of election key :param sender_guardian_polynomial: The owner's Election polynomial :param receiver_guardian_public_key: The receiving guardian's public key :return: Election partial key backup """ coordinate = compute_polynomial_coordinate( receiver_guardian_public_key.sequence_order, sender_guardian_polynomial ) coordinate_data = CoordinateData(coordinate) nonce = rand_q() seed = get_backup_seed( receiver_guardian_public_key.owner_id, receiver_guardian_public_key.sequence_order, ) encrypted_coordinate = hashed_elgamal_encrypt( coordinate_data.to_bytes(), nonce, receiver_guardian_public_key.key, seed, ) return ElectionPartialKeyBackup( sender_guardian_id, receiver_guardian_public_key.owner_id, receiver_guardian_public_key.sequence_order, encrypted_coordinate, ) def get_backup_seed(receiver_guardian_id: str, sequence_order: int) -> ElementModQ: return hash_elems(receiver_guardian_id, sequence_order) def verify_election_partial_key_backup( receiver_guardian_id: str, sender_guardian_backup: ElectionPartialKeyBackup, sender_guardian_public_key: ElectionPublicKey, receiver_guardian_keys: ElectionKeyPair, ) -> ElectionPartialKeyVerification: """ Verify election partial key backup contain point on owners polynomial :param receiver_guardian_id: Receiving guardian's identifier :param sender_guardian_backup: Sender guardian's election partial key backup :param sender_guardian_public_key: Sender guardian's election public key :param receiver_guardian_keys: Receiving guardian's key pair """ encryption_seed = get_backup_seed( receiver_guardian_id, sender_guardian_backup.designated_sequence_order, ) secret_key = receiver_guardian_keys.key_pair.secret_key bytes_optional = sender_guardian_backup.encrypted_coordinate.decrypt( secret_key, encryption_seed ) coordinate_data: CoordinateData = CoordinateData.from_bytes( get_optional(bytes_optional) ) verified = verify_polynomial_coordinate( coordinate_data.coordinate, sender_guardian_backup.designated_sequence_order, sender_guardian_public_key.coefficient_commitments, ) return ElectionPartialKeyVerification( sender_guardian_backup.owner_id, sender_guardian_backup.designated_id, receiver_guardian_id, verified, ) def generate_election_partial_key_challenge( backup: ElectionPartialKeyBackup, polynomial: ElectionPolynomial, ) -> ElectionPartialKeyChallenge: """ Generate challenge to a previous verification of a partial key backup :param backup: Election partial key backup in question :param polynomial: Polynomial to regenerate point :return: Election partial key verification """ return ElectionPartialKeyChallenge( backup.owner_id, backup.designated_id, backup.designated_sequence_order, compute_polynomial_coordinate(backup.designated_sequence_order, polynomial), polynomial.get_commitments(), polynomial.get_proofs(), ) def verify_election_partial_key_challenge( verifier_id: VerifierId, challenge: ElectionPartialKeyChallenge ) -> ElectionPartialKeyVerification: """ Verify a challenge to a previous verification of a partial key backup :param verifier_id: Verifier of the challenge :param challenge: Election partial key challenge :return: Election partial key verification """ return ElectionPartialKeyVerification( challenge.owner_id, challenge.designated_id, verifier_id, verify_polynomial_coordinate( challenge.value, challenge.designated_sequence_order, challenge.coefficient_commitments, ), ) def combine_election_public_keys( election_public_keys: List[ElectionPublicKey], ) -> ElectionJointKey: """ Creates a joint election key from the public keys of all guardians :param election_public_keys: all public keys of the guardians :return: ElectionJointKey for election """ public_keys = [set.key for set in election_public_keys] commitments = [ commitment for set in election_public_keys for commitment in set.coefficient_commitments ] return ElectionJointKey( joint_public_key=elgamal_combine_public_keys(public_keys), commitment_hash=get_optional( hash_elems(commitments) ), # H(K 1,0 , K 2,0 ... , K n,0 ) ) ================================================ FILE: src/electionguard/key_ceremony_mediator.py ================================================ from dataclasses import dataclass, field from typing import Dict, Iterable, List, Optional from .key_ceremony import ( CeremonyDetails, ElectionJointKey, ElectionPartialKeyBackup, ElectionPartialKeyChallenge, ElectionPartialKeyVerification, ElectionPublicKey, combine_election_public_keys, verify_election_partial_key_challenge, ) from .type import GuardianId, MediatorId @dataclass(unsafe_hash=True) class GuardianPair: """Pair of guardians involved in sharing""" owner_id: GuardianId designated_id: GuardianId @dataclass class BackupVerificationState: """The state of the verifications of all guardian election partial key backups""" all_sent: bool = field(default=False) all_verified: bool = field(default=False) failed_verifications: List[GuardianPair] = field(default_factory=list) class KeyCeremonyMediator: """ KeyCeremonyMediator for assisting communication between guardians """ id: MediatorId ceremony_details: CeremonyDetails # From Guardians # Round 1 _election_public_keys: Dict[GuardianId, ElectionPublicKey] # Round 2 _election_partial_key_backups: Dict[GuardianPair, ElectionPartialKeyBackup] # Round 3 _election_partial_key_verifications: Dict[ GuardianPair, ElectionPartialKeyVerification ] def __init__(self, id: MediatorId, ceremony_details: CeremonyDetails): self.id = id self.ceremony_details = ceremony_details self._election_public_keys: Dict[GuardianId, ElectionPublicKey] = {} self._election_partial_key_backups: Dict[ GuardianPair, ElectionPartialKeyBackup ] = {} self._election_partial_key_verifications: Dict[ GuardianPair, ElectionPartialKeyVerification ] = {} self._election_partial_key_challenges: Dict[ GuardianPair, ElectionPartialKeyChallenge ] = {} # ROUND 1: Announce guardians with public keys def announce(self, key: ElectionPublicKey) -> None: """ Announce the guardian as present and participating the Key Ceremony :param key: Guardian's election public key """ self._receive_election_public_key(key) def all_guardians_announced(self) -> bool: """ Check the annoucement of all the guardians expected :return: True if all guardians in attendance are announced """ return ( len(self._election_public_keys) == self.ceremony_details.number_of_guardians ) def share_announced( self, requesting_guardian_id: Optional[GuardianId] = None ) -> Optional[List[ElectionPublicKey]]: """ When all guardians have announced, share their public keys indicating their announcement """ if not self.all_guardians_announced(): return None guardian_keys: List[ElectionPublicKey] = [] for guardian_id in self._get_announced_guardians(): if guardian_id != requesting_guardian_id: guardian_keys.append(self._election_public_keys[guardian_id]) return guardian_keys # ROUND 2: Share Election Partial Key Backups for compensating def receive_backups(self, backups: List[ElectionPartialKeyBackup]) -> None: """ Receive all the election partial key backups generated by a guardian """ if not self.all_guardians_announced(): return for backup in backups: self._receive_election_partial_key_backup(backup) def all_backups_available(self) -> bool: """ Check the availability of all the guardians backups :return: True if all guardians have sent backups """ return ( self.all_guardians_announced() and self._all_election_partial_key_backups_available() ) def share_backups( self, requesting_guardian_id: Optional[GuardianId] = None ) -> Optional[List[ElectionPartialKeyBackup]]: """ Share all backups designated for a specific guardian """ if not self.all_guardians_announced() or not self.all_backups_available(): return None if not requesting_guardian_id: return list(self._election_partial_key_backups.values()) return self._share_election_partial_key_backups_to_guardian( requesting_guardian_id ) # ROUND 3: Share verifications of backups def receive_backup_verifications( self, verifications: List[ElectionPartialKeyVerification] ) -> None: """ Receive all the election partial key verifications performed by a guardian """ if not self.all_backups_available(): return for verification in verifications: self._receive_election_partial_key_verification(verification) def get_verification_state(self) -> BackupVerificationState: if ( not self.all_backups_available() or not self._all_election_partial_key_verifications_received() ): return BackupVerificationState() return self._check_verification_of_election_partial_key_backups() def all_backups_verified(self) -> bool: return self.get_verification_state().all_verified # ROUND 4 (Optional): If a verification fails, guardian must issue challenge def verify_challenge( self, challenge: ElectionPartialKeyChallenge ) -> ElectionPartialKeyVerification: """ Mediator receives challenge and will act to mediate and verify """ verification = verify_election_partial_key_challenge(self.id, challenge) if verification.verified: self._receive_election_partial_key_verification(verification) return verification # FINAL: Publish joint public election key def publish_joint_key(self) -> Optional[ElectionJointKey]: """ Publish joint election key from the public keys of all guardians :return: Joint key for election """ if not self.all_backups_verified(): return None return combine_election_public_keys(list(self._election_public_keys.values())) def reset(self, ceremony_details: CeremonyDetails) -> None: """ Reset mediator to initial state :param ceremony_details: Ceremony details of election """ self.ceremony_details = ceremony_details self._election_public_keys = {} self._election_partial_key_backups = {} self._election_partial_key_challenges = {} self._election_partial_key_verifications = {} # Election Public Keys def _receive_election_public_key(self, public_key: ElectionPublicKey) -> None: """ Receive election public key from guardian :param public_key: election public key """ self._election_public_keys[public_key.owner_id] = public_key def _get_announced_guardians(self) -> Iterable[GuardianId]: return self._election_public_keys.keys() # Election Partial Key Backups def _receive_election_partial_key_backup( self, backup: ElectionPartialKeyBackup ) -> None: """ Receive election partial key backup from guardian :param backup: Election partial key backup :return: boolean indicating success or failure """ if backup.owner_id == backup.designated_id: return self._election_partial_key_backups[ GuardianPair(backup.owner_id, backup.designated_id) ] = backup def _all_election_partial_key_backups_available(self) -> bool: """ True if all election partial key backups for all guardians available :return: All election partial key backups for all guardians available """ required_backups_per_guardian = self.ceremony_details.number_of_guardians - 1 return ( len(self._election_partial_key_backups) == required_backups_per_guardian * self.ceremony_details.number_of_guardians ) def _share_election_partial_key_backups_to_guardian( self, guardian_id: GuardianId ) -> List[ElectionPartialKeyBackup]: """ Share all election partial key backups for designated guardian :param guardian_id: Recipients guardian id :return: List of guardians designated backups """ backups: List[ElectionPartialKeyBackup] = [] for current_guardian_id in self._get_announced_guardians(): if guardian_id != current_guardian_id: backup = self._election_partial_key_backups[ GuardianPair(current_guardian_id, guardian_id) ] if backup is not None: backups.append(backup) return backups # Partial Key Verifications def _receive_election_partial_key_verification( self, verification: ElectionPartialKeyVerification ) -> None: """ Receive election partial key verification from guardian :param verification: Election partial key verification """ if verification.owner_id == verification.designated_id: return self._election_partial_key_verifications[ GuardianPair(verification.owner_id, verification.designated_id) ] = verification def _all_election_partial_key_verifications_received(self) -> bool: """ True if all election partial key verifications recieved :return: All election partial key verifications received """ required_verifications_per_guardian = ( self.ceremony_details.number_of_guardians - 1 ) return ( len(self._election_partial_key_verifications) == required_verifications_per_guardian * self.ceremony_details.number_of_guardians ) def _check_verification_of_election_partial_key_backups( self, ) -> BackupVerificationState: """ True if all election partial key backups verified :return: All election partial key backups verified """ if not self._all_election_partial_key_verifications_received(): return BackupVerificationState() failed_verifications: List[GuardianPair] = [] for verification in self._election_partial_key_verifications.values(): if not verification.verified: failed_verifications.append( GuardianPair(verification.owner_id, verification.designated_id) ) return BackupVerificationState( True, len(failed_verifications) == 0, failed_verifications ) ================================================ FILE: src/electionguard/logs.py ================================================ import inspect import logging import os.path import sys from typing import Any, List, Tuple from logging.handlers import RotatingFileHandler from .singleton import Singleton FORMAT = "[%(process)d:%(asctime)s]:%(levelname)s:%(message)s" class ElectionGuardLog(Singleton): """ A singleton log for the library """ __logger: logging.Logger __stream_handler: logging.StreamHandler def __init__(self) -> None: super().__init__() self.__logger = logging.getLogger("electionguard") # Pythong's logger will use the most restrictive of the logger level and the handler level, # so set the logger to the lowest level the handler ever might log at self.__logger.setLevel(logging.DEBUG) self.__stream_handler = get_stream_handler(logging.INFO) self.__logger.addHandler(self.__stream_handler) @staticmethod def __get_call_info() -> Tuple[str, str, int]: stack = inspect.stack() # stack[0]: __get_call_info # stack[1]: __formatted_message # stack[2]: (log method, e.g. "warn") # stack[3]: Singleton # stack[4]: caller <-- we want this filename = stack[4][1] line = stack[4][2] funcname = stack[4][3] return filename, funcname, line def __formatted_message(self, message: str) -> str: filename, funcname, line = self.__get_call_info() message = f"{os.path.basename(filename)}.{funcname}:#L{line}: {message}" return message def set_stream_log_level(self, Level: int) -> None: """ Sets the stream log level """ self.remove_handler(self.__stream_handler) self.add_handler(get_stream_handler(Level)) def add_handler(self, handler: logging.Handler) -> None: """ Adds a logger handler """ self.__logger.addHandler(handler) def remove_handler(self, handler: logging.Handler) -> None: """ Removes a logger handler """ self.__logger.removeHandler(handler) def handlers(self) -> List[logging.Handler]: """ Returns all logging handlers """ return self.__logger.handlers def debug(self, message: str, *args: Any, **kwargs: Any) -> None: """ Logs a debug message """ self.__logger.debug(self.__formatted_message(message), *args, **kwargs) def info(self, message: str, *args: Any, **kwargs: Any) -> None: """ Logs a info message """ self.__logger.info(self.__formatted_message(message), *args, **kwargs) def warn(self, message: str, *args: Any, **kwargs: Any) -> None: """ Logs a warning message """ self.__logger.warning(self.__formatted_message(message), *args, **kwargs) def error(self, message: str, *args: Any, **kwargs: Any) -> None: """ Logs a error message """ self.__logger.error(self.__formatted_message(message), *args, **kwargs) def critical(self, message: str, *args: Any, **kwargs: Any) -> None: """ Logs a critical message """ self.__logger.critical(self.__formatted_message(message), *args, **kwargs) def get_stream_handler(log_level: int) -> logging.StreamHandler: """ Get a Stream Handler, sends only warnings and errors to stdout. """ stream_handler = logging.StreamHandler(sys.stdout) stream_handler.setLevel(log_level) stream_handler.setFormatter(logging.Formatter(FORMAT)) return stream_handler def get_file_handler(log_level: int, filename: str) -> logging.FileHandler: """ Get a File System Handler, sends verbose logging to a file, `electionguard.log`. When that file gets too large, the logs will rotate, creating files with names like `electionguard.log.1`. """ # TODO: add file compression, save a bunch of space. # https://medium.com/@rahulraghu94/overriding-pythons-timedrotatingfilehandler-to-compress-your-log-files-iot-c766a4ace240 # pylint: disable=line-too-long file_handler = RotatingFileHandler( filename, "a", maxBytes=10_000_000, backupCount=10 ) file_handler.setLevel(log_level) file_handler.setFormatter(logging.Formatter(FORMAT)) return file_handler LOG = ElectionGuardLog() def log_add_handler(handler: logging.Handler) -> None: """ Adds a handler to the logger """ LOG.add_handler(handler) def log_remove_handler(handler: logging.Handler) -> None: """ Removes a handler from the logger """ LOG.remove_handler(handler) def log_handlers() -> List[logging.Handler]: """ Returns all logger handlers """ return LOG.handlers() def log_debug(msg: str, *args: Any, **kwargs: Any) -> None: """ Logs a debug message to the console and the file log. """ LOG.debug(msg, *args, **kwargs) def log_info(msg: str, *args: Any, **kwargs: Any) -> None: """ Logs an information message to the console and the file log. """ LOG.info(msg, *args, **kwargs) def log_warning(msg: str, *args: Any, **kwargs: Any) -> None: """ Logs a warning message to the console and the file log. """ LOG.warn(msg, *args, **kwargs) def log_error(msg: str, *args: Any, **kwargs: Any) -> None: """ Logs an error message to the console and the file log. """ LOG.error(msg, *args, **kwargs) def log_critical(msg: str, *args: Any, **kwargs: Any) -> None: """ Logs a critical message to the console and the file log. """ LOG.critical(msg, *args, **kwargs) ================================================ FILE: src/electionguard/manifest.py ================================================ from dataclasses import dataclass, field, InitVar from datetime import datetime from enum import Enum, unique from typing import Dict, cast, List, Optional, Set, Any from .election_object_base import ElectionObjectBase, OrderedObjectBase, list_eq from .group import ElementModQ from .hash import CryptoHashable, hash_elems from .logs import log_warning from .utils import get_optional, to_iso_date_string @unique class ElectionType(Enum): """ enumerations for the `ElectionReport` entity see: https://developers.google.com/elections-data/reference/election-type """ unknown = "unknown" general = "general" partisan_primary_closed = "partisan_primary_closed" partisan_primary_open = "partisan_primary_open" primary = "primary" runoff = "runoff" special = "special" other = "other" @unique class ReportingUnitType(Enum): """ Enumeration for the type of geopolitical unit see: https://developers.google.com/elections-data/reference/reporting-unit-type """ unknown = "unknown" ballot_batch = "ballot_batch" ballot_style_area = "ballot_style_area" borough = "borough" city = "city" city_council = "city_council" combined_precinct = "combined_precinct" congressional = "congressional" country = "country" county = "county" county_council = "county_council" drop_box = "drop_box" judicial = "judicial" municipality = "municipality" polling_place = "polling_place" precinct = "precinct" school = "school" special = "special" split_precinct = "split_precinct" state = "state" state_house = "state_house" state_senate = "state_senate" township = "township" utility = "utility" village = "village" vote_center = "vote_center" ward = "ward" water = "water" other = "other" @unique class VoteVariationType(Enum): """ Enumeration for contest algorithm or rules in the `Contest` entity see: https://developers.google.com/elections-data/reference/vote-variation """ one_of_m = "one_of_m" approval = "approval" borda = "borda" cumulative = "cumulative" majority = "majority" n_of_m = "n_of_m" plurality = "plurality" proportional = "proportional" range = "range" rcv = "rcv" super_majority = "super_majority" other = "other" SUPPORTED_VOTE_VARIATIONS = [ VoteVariationType.one_of_m, VoteVariationType.approval, VoteVariationType.majority, VoteVariationType.n_of_m, VoteVariationType.plurality, VoteVariationType.super_majority, ] # pylint: disable=super-init-not-called @dataclass(eq=True, unsafe_hash=True) class AnnotatedString(CryptoHashable): """ Use this as a type for character strings. See: https://developers.google.com/elections-data/reference/annotated-string """ annotation: str = field(default="") value: str = field(default="") # explicit `__init__` added as workaround for https://bugs.python.org/issue45081 # can potentially be removed with future python version >3.9.7 def __init__( self, annotation: str = "", value: str = "", ): self.annotation = annotation self.value = value def crypto_hash(self) -> ElementModQ: """ A hash representation of the object """ hash = hash_elems(self.annotation, self.value) return hash # pylint: disable=super-init-not-called @dataclass(eq=True, unsafe_hash=True) class Language(CryptoHashable): """ The ISO-639 language see: https://en.wikipedia.org/wiki/ISO_639 """ value: str language: str = field(default="en") # explicit `__init__` added as workaround for https://bugs.python.org/issue45081 # can potentially be removed with future python version >3.9.7 def __init__( self, value: str, language: str = "en", ): self.value = value self.language = language def crypto_hash(self) -> ElementModQ: """ A hash representation of the object """ hash = hash_elems(self.value, self.language) return hash # pylint: disable=super-init-not-called @dataclass(eq=True, unsafe_hash=True) class InternationalizedText(CryptoHashable): """ Data entity used to represent multi-national text. Use when text on a ballot contains multi-national text. See: https://developers.google.com/elections-data/reference/internationalized-text """ text: List[Language] = field(default_factory=lambda: []) # explicit `__init__` added as workaround for https://bugs.python.org/issue45081 # can potentially be removed with future python version >3.9.7 def __init__( self, text: Optional[List[Language]] = None, ): self.text = text if text else [] def crypto_hash(self) -> ElementModQ: """ A hash representation of the object """ hash = hash_elems(self.text) return hash # pylint: disable=super-init-not-called @dataclass(eq=True, unsafe_hash=True) class ContactInformation(CryptoHashable): """ For defining contact information about objects such as persons, boards of authorities, and organizations. See: https://developers.google.com/elections-data/reference/contact-information """ address_line: Optional[List[str]] = field(default=None) email: Optional[List[AnnotatedString]] = field(default=None) phone: Optional[List[AnnotatedString]] = field(default=None) name: Optional[str] = field(default=None) # explicit `__init__` added as workaround for https://bugs.python.org/issue45081 # can potentially be removed with future python version >3.9.7 def __init__( self, address_line: Optional[List[str]] = None, email: Optional[List[AnnotatedString]] = None, phone: Optional[List[AnnotatedString]] = None, name: Optional[str] = None, ): self.address_line = address_line self.email = email self.phone = phone self.name = name def crypto_hash(self) -> ElementModQ: """ A hash representation of the object """ hash = hash_elems(self.name, self.address_line, self.email, self.phone) return hash @dataclass(eq=True, unsafe_hash=True) class GeopoliticalUnit(ElectionObjectBase, CryptoHashable): """ Use this entity for defining geopolitical units such as cities, districts, jurisdictions, or precincts, for the purpose of associating contests, offices, vote counts, or other information with the geographies. See: https://developers.google.com/elections-data/reference/gp-unit """ name: str type: ReportingUnitType contact_information: Optional[ContactInformation] = field(default=None) def crypto_hash(self) -> ElementModQ: """ A hash representation of the object """ hash = hash_elems( self.object_id, self.name, str(self.type.name), self.contact_information ) return hash @dataclass(eq=True, unsafe_hash=True) class BallotStyle(ElectionObjectBase, CryptoHashable): """ A BallotStyle works as a key to uniquely specify a set of contests. See also `ContestDescription`. """ geopolitical_unit_ids: Optional[List[str]] = field(default=None) party_ids: Optional[List[str]] = field(default=None) image_uri: Optional[str] = field(default=None) def crypto_hash(self) -> ElementModQ: """ A hash representation of the object """ hash = hash_elems( self.object_id, self.geopolitical_unit_ids, self.party_ids, self.image_uri ) return hash @dataclass(eq=True, unsafe_hash=True) class Party(ElectionObjectBase, CryptoHashable): """ Use this entity to describe a political party that can then be referenced from other entities. See: https://developers.google.com/elections-data/reference/party """ name: InternationalizedText = field(default=InternationalizedText()) abbreviation: Optional[str] = field(default=None) color: Optional[str] = field(default=None) logo_uri: Optional[str] = field(default=None) def get_party_id(self) -> str: """ Returns the object identifier associated with the Party. """ return self.object_id def crypto_hash(self) -> ElementModQ: """ A hash representation of the object """ hash = hash_elems( self.object_id, self.name, self.abbreviation, self.color, self.logo_uri, ) return hash @dataclass(eq=True, unsafe_hash=True) class Candidate(ElectionObjectBase, CryptoHashable): """ Entity describing information about a candidate in a contest. See: https://developers.google.com/elections-data/reference/candidate Note: The ElectionGuard Data Spec deviates from the NIST model in that selections for any contest type are considered a "candidate". for instance, on a yes-no referendum contest, two `candidate` objects would be included in the model to represent the `affirmative` and `negative` selections for the contest. See the wiki, readme's, and tests in this repo for more info """ name: InternationalizedText = field(default=InternationalizedText()) party_id: Optional[str] = field(default=None) image_uri: Optional[str] = field(default=None) is_write_in: Optional[bool] = field(default=None) def get_candidate_id(self) -> str: """ Given a `Candidate`, returns a "candidate ID", which is used in other ElectionGuard structures. """ return self.object_id def crypto_hash(self) -> ElementModQ: """ A hash representation of the object """ hash = hash_elems(self.object_id, self.name, self.party_id, self.image_uri) return hash @dataclass(eq=True, unsafe_hash=True) class SelectionDescription(OrderedObjectBase, CryptoHashable): """ Data entity for the ballot selections in a contest, for example linking candidates and parties to their vote counts. See: https://developers.google.com/elections-data/reference/ballot-selection Note: The ElectionGuard Data Spec deviates from the NIST model in that there is no difference for different types of selections. The ElectionGuard Data Spec deviates from the NIST model in that `sequence_order` is a required field since it is used for ordering selections in a contest to ensure various encryption primitives are deterministic. For a given election, the sequence of selections displayed to a user may be different however that information is not captured by default when encrypting a specific ballot. """ candidate_id: str def crypto_hash(self) -> ElementModQ: """ A hash representation of the object """ hash = hash_elems(self.object_id, self.sequence_order, self.candidate_id) return hash # pylint: disable=too-many-instance-attributes @dataclass(unsafe_hash=True) class ContestDescription(OrderedObjectBase, CryptoHashable): """ Use this data entity for describing a contest and linking the contest to the associated candidates and parties. See: https://developers.google.com/elections-data/reference/contest Note: The ElectionGuard Data Spec deviates from the NIST model in that `sequence_order` is a required field since it is used for ordering selections in a contest to ensure various encryption primitives are deterministic. For a given election, the sequence of contests displayed to a user may be different however that information is not captured by default when encrypting a specific ballot. """ electoral_district_id: str vote_variation: VoteVariationType # Number of candidates that are elected in the contest ("n" of n-of-m). # Note: a referendum is considered a specific case of 1-of-m in ElectionGuard number_elected: int # Maximum number of votes/write-ins per voter in this contest. Used in cumulative voting # to indicate how many total votes a voter can spread around. In n-of-m elections, this will # be None. votes_allowed: Optional[int] # Name of the contest, not necessarily as it appears on the ballot. name: str # For associating a ballot selection for the contest, i.e., a candidate, a ballot measure. ballot_selections: List[SelectionDescription] = field(default_factory=lambda: []) # Title of the contest as it appears on the ballot. ballot_title: Optional[InternationalizedText] = field(default=None) # Subtitle of the contest as it appears on the ballot. ballot_subtitle: Optional[InternationalizedText] = field(default=None) def __eq__(self, other: Any) -> bool: return ( isinstance(other, ContestDescription) and self.electoral_district_id == other.electoral_district_id and self.sequence_order == other.sequence_order and self.vote_variation == other.vote_variation and self.number_elected == other.number_elected and self.votes_allowed == other.votes_allowed and self.name == other.name and list_eq(self.ballot_selections, other.ballot_selections) and self.ballot_title == other.ballot_title and self.ballot_subtitle == other.ballot_subtitle ) def crypto_hash(self) -> ElementModQ: """ Given a ContestDescription, deterministically derives a "hash" of that contest, suitable for use in ElectionGuard's "base hash" values, and for validating that either a plaintext or encrypted voted context and its corresponding contest description match up. """ # remove any placeholders from the hash mechanism hash = hash_elems( self.object_id, self.sequence_order, self.electoral_district_id, str(self.vote_variation.name), self.ballot_title, self.ballot_subtitle, self.name, self.number_elected, self.votes_allowed, self.ballot_selections, ) return hash def is_valid(self) -> bool: """ Check the validity of the contest object by verifying its data """ contest_has_valid_number_elected = self.number_elected <= len( self.ballot_selections ) contest_has_valid_votes_allowed = ( self.votes_allowed is None or self.number_elected <= self.votes_allowed ) # verify the candidate_ids, selection object_ids, and sequence_ids are unique candidate_ids: Set[str] = set() selection_ids: Set[str] = set() sequence_ids: Set[int] = set() selection_count = 0 expected_selection_count = len(self.ballot_selections) for selection in self.ballot_selections: selection_count += 1 # validate the object_id if selection.object_id not in selection_ids: selection_ids.add(selection.object_id) # validate the sequence_order if selection.sequence_order not in sequence_ids: sequence_ids.add(selection.sequence_order) # validate the candidate id if selection.candidate_id not in candidate_ids: candidate_ids.add(selection.candidate_id) selections_have_valid_candidate_ids = ( len(candidate_ids) == expected_selection_count ) selections_have_valid_selection_ids = ( len(selection_ids) == expected_selection_count ) selections_have_valid_sequence_ids = ( len(sequence_ids) == expected_selection_count ) contest_has_supported_vote_variation = ( self.vote_variation in SUPPORTED_VOTE_VARIATIONS ) success = ( contest_has_valid_number_elected and contest_has_valid_votes_allowed and selections_have_valid_candidate_ids and selections_have_valid_selection_ids and selections_have_valid_sequence_ids and contest_has_supported_vote_variation ) if not success: log_warning( "Contest %s failed validation check: %s", self.object_id, str( { "contest_has_valid_number_elected": contest_has_valid_number_elected, "contest_has_valid_votes_allowed": contest_has_valid_votes_allowed, "selections_have_valid_candidate_ids": selections_have_valid_candidate_ids, "selections_have_valid_selection_ids": selections_have_valid_selection_ids, "selections_have_valid_sequence_ids": selections_have_valid_sequence_ids, "contest_has_supported_vote_variation": contest_has_supported_vote_variation, } ), ) return success @dataclass(eq=True, unsafe_hash=True) class CandidateContestDescription(ContestDescription): """ Use this entity to describe a contest that involves selecting one or more candidates. See: https://developers.google.com/elections-data/reference/contest Note: The ElectionGuard Data Spec deviates from the NIST model in that this subclass is used purely for convenience """ primary_party_ids: List[str] = field(default_factory=lambda: []) @dataclass(eq=True, unsafe_hash=True) class ReferendumContestDescription(ContestDescription): """ Use this entity to describe a contest that involves selecting exactly one 'candidate'. See: https://developers.google.com/elections-data/reference/contest Note: The ElectionGuard Data Spec deviates from the NIST model in that this subclass is used purely for convenience """ @dataclass(eq=True, unsafe_hash=True) class ContestDescriptionWithPlaceholders(ContestDescription): """ ContestDescriptionWithPlaceholders is a `ContestDescription` with ElectionGuard `placeholder_selections`. (The ElectionGuard spec requires for n-of-m elections that there be *exactly* n counters that are one with the rest zero, so if a voter deliberately undervotes, one or more of the placeholder counters will become one. This allows the `ConstantChaumPedersenProof` to verify correctly for undervoted contests.) """ placeholder_selections: List[SelectionDescription] = field( default_factory=lambda: [] ) def is_valid(self) -> bool: """ Checks is contest description is valid :return: true if valid """ contest_description_validates = super().is_valid() return ( contest_description_validates and len(self.placeholder_selections) == self.number_elected ) def is_placeholder(self, selection: SelectionDescription) -> bool: """ Checks is contest description is placeholder :return: true if placeholder """ return selection in self.placeholder_selections def selection_for(self, selection_id: str) -> Optional[SelectionDescription]: """ Gets the description for a particular id :param selection_id: Id of Selection :return: description """ matching_selections = list( filter(lambda i: i.object_id == selection_id, self.ballot_selections) ) if any(matching_selections): return matching_selections[0] matching_placeholders = list( filter(lambda i: i.object_id == selection_id, self.placeholder_selections) ) if any(matching_placeholders): return matching_placeholders[0] return None class SpecVersion(Enum): """Specify ElectionGuard Versions""" EG0_95 = "v0.95" EG1_0 = "1.0" # pylint: disable=too-many-instance-attributes,super-init-not-called @dataclass(unsafe_hash=True) class Manifest(CryptoHashable): """ Use this entity for defining the structure of the election and associated information such as candidates, contests, and vote counts. This class is based on the NIST Election Common Standard Data Specification. Some deviations from the standard exist. This structure is considered an immutable input object and should not be changed through the course of an election, as it's hash representation is the basis for all other hash representations within an ElectionGuard election context. See: https://developers.google.com/elections-data/reference/election """ election_scope_id: str spec_version: SpecVersion type: ElectionType start_date: datetime end_date: datetime geopolitical_units: List[GeopoliticalUnit] parties: List[Party] candidates: List[Candidate] contests: List[ContestDescription] ballot_styles: List[BallotStyle] name: Optional[InternationalizedText] = field(default=None) contact_information: Optional[ContactInformation] = field(default=None) # explicit `__init__` added as workaround for https://bugs.python.org/issue45081 # can potentially be removed with future python version >3.9.7 def __init__( self, election_scope_id: str, spec_version: SpecVersion, type: ElectionType, start_date: datetime, end_date: datetime, geopolitical_units: List[GeopoliticalUnit], parties: List[Party], candidates: List[Candidate], contests: List[ContestDescription], ballot_styles: List[BallotStyle], name: Optional[InternationalizedText] = None, contact_information: Optional[ContactInformation] = None, ): self.election_scope_id = election_scope_id self.spec_version = spec_version self.type = type self.start_date = start_date self.end_date = end_date self.geopolitical_units = geopolitical_units self.parties = parties self.candidates = candidates self.contests = contests self.ballot_styles = ballot_styles self.name = name self.contact_information = contact_information def __eq__(self, other: Any) -> bool: return ( isinstance(other, Manifest) and self.election_scope_id == other.election_scope_id and self.type == other.type and self.start_date == other.start_date and self.end_date == other.end_date and list_eq(self.geopolitical_units, other.geopolitical_units) and list_eq(self.parties, other.parties) and list_eq(self.candidates, other.candidates) and list_eq(self.contests, other.contests) and list_eq(self.ballot_styles, other.ballot_styles) and self.name == other.name and self.contact_information == other.contact_information ) def crypto_hash(self) -> ElementModQ: """ Returns a hash of the metadata components of the election """ hash = hash_elems( self.election_scope_id, str(self.type.name), to_iso_date_string(self.start_date), to_iso_date_string(self.end_date), self.name, self.contact_information, self.geopolitical_units, self.parties, self.contests, self.ballot_styles, ) return hash def is_valid(self) -> bool: """ Verifies the dataset to ensure it is well-formed """ gp_unit_ids: Set[str] = set() ballot_style_ids: Set[str] = set() party_ids: Set[str] = set() candidate_ids: Set[str] = set() contest_ids: Set[str] = set() # Validate GP Units for gp_unit in self.geopolitical_units: if gp_unit.object_id not in gp_unit_ids: gp_unit_ids.add(gp_unit.object_id) # fail if there are duplicates geopolitical_units_valid = len(gp_unit_ids) == len(self.geopolitical_units) # Validate Ballot Styles ballot_styles_have_valid_gp_unit_ids = True for style in self.ballot_styles: if style.object_id not in ballot_style_ids: ballot_style_ids.add(style.object_id) if style.geopolitical_unit_ids is None: ballot_styles_have_valid_gp_unit_ids = False break # validate associated gp unit ids for gp_unit_id in style.geopolitical_unit_ids: ballot_styles_have_valid_gp_unit_ids = ( ballot_styles_have_valid_gp_unit_ids and gp_unit_id in gp_unit_ids ) ballot_styles_valid = ( len(ballot_style_ids) == len(self.ballot_styles) and ballot_styles_have_valid_gp_unit_ids ) # Validate Parties for party in self.parties: if party.object_id not in party_ids: party_ids.add(party.object_id) parties_valid = len(party_ids) == len(self.parties) # Validate Candidates candidates_have_valid_party_ids = True for candidate in self.candidates: if candidate.object_id not in candidate_ids: candidate_ids.add(candidate.object_id) # validate the associated party id candidates_have_valid_party_ids = candidates_have_valid_party_ids and ( candidate.party_id is None or candidate.party_id in party_ids ) candidates_have_valid_length = len(candidate_ids) == len(self.candidates) candidates_valid = ( candidates_have_valid_length and candidates_have_valid_party_ids ) # Validate Contests contests_validate_their_properties = True contests_have_valid_electoral_district_id = True candidate_contests_have_valid_party_ids = True contest_sequence_ids: Set[int] = set() for contest in self.contests: contests_validate_their_properties = ( contests_validate_their_properties and contest.is_valid() ) if contest.object_id not in contest_ids: contest_ids.add(contest.object_id) # validate the sequence order if contest.sequence_order not in contest_sequence_ids: contest_sequence_ids.add(contest.sequence_order) # validate the associated gp unit id contests_have_valid_electoral_district_id = ( contests_have_valid_electoral_district_id and contest.electoral_district_id in gp_unit_ids ) if isinstance(contest, CandidateContestDescription): candidate_contest = cast(CandidateContestDescription, contest) if candidate_contest.primary_party_ids is not None: for primary_party_id in candidate_contest.primary_party_ids: # validate the party ids candidate_contests_have_valid_party_ids = ( candidate_contests_have_valid_party_ids and primary_party_id in party_ids ) # TODO: ISSUE #55: verify that the contest sequence order set is in the proper order contests_have_valid_object_ids = len(contest_ids) == len(self.contests) contests_have_valid_sequence_ids = len(contest_sequence_ids) == len( self.contests ) contests_valid = ( contests_have_valid_object_ids and contests_have_valid_sequence_ids and contests_validate_their_properties and contests_have_valid_electoral_district_id and candidate_contests_have_valid_party_ids ) success = ( geopolitical_units_valid and ballot_styles_valid and parties_valid and candidates_valid and contests_valid ) if not success: log_warning( "Election failed validation check: is_valid: %s", str( { "geopolitical_units_valid": geopolitical_units_valid, "ballot_styles_valid": ballot_styles_valid, "ballot_styles_have_valid_gp_unit_ids": ballot_styles_have_valid_gp_unit_ids, "parties_valid": parties_valid, "candidates_valid": candidates_valid, "candidates_have_valid_length": candidates_have_valid_length, "candidates_have_valid_party_ids": candidates_have_valid_party_ids, "contests_valid": contests_valid, "contests_have_valid_object_ids": contests_have_valid_object_ids, "contests_have_valid_sequence_ids": contests_have_valid_sequence_ids, "contests_validate_their_properties": contests_validate_their_properties, "contests_have_valid_electoral_district_id": contests_have_valid_electoral_district_id, "candidate_contests_have_valid_party_ids": candidate_contests_have_valid_party_ids, } ), ) return success def _get_candidate_name(self, candidate: Candidate, lang: str) -> str: if candidate.is_write_in: return "Write-In" return get_i8n_value(candidate.name, lang, candidate.object_id) def _get_candidate_names(self, lang: str) -> Dict[str, str]: return { candidate.object_id: self._get_candidate_name(candidate, lang) for candidate in self.candidates } def _get_selections_with_candidate_id(self) -> Dict[str, str]: selections: Dict[str, str] = {} for contest in self.contests: for selection in contest.ballot_selections: selections.update({selection.object_id: selection.candidate_id}) return selections def _replace_candidate_ids_with_names( self, selections: Dict[str, str], candidates: Dict[str, str] ) -> None: for selection_id, candidate_id in selections.items(): candidate_name = candidates.get(candidate_id) if candidate_name is not None: selections.update({selection_id: candidate_name}) def get_selection_names(self, lang: str) -> Dict[str, str]: """ Retrieves a dictionary whose keys are all selection id's and whose values are those selection's candidate names in the supplied language if available """ candidates = self._get_candidate_names(lang) selections = self._get_selections_with_candidate_id() self._replace_candidate_ids_with_names(selections, candidates) return selections def get_contest_names(self) -> Dict[str, str]: """ Retrieves a dictionary whose keys are all contest id's and whose values are those contest's names """ return {contest.object_id: contest.name for contest in self.contests} def get_name(self) -> str: def get_first_value(text: Optional[InternationalizedText]) -> str: return "" if text is None else text.text[0].value return get_first_value(self.name) @dataclass(eq=True, unsafe_hash=True) class InternalManifest: """ `InternalManifest` is a subset of the `Manifest` structure that specifies the components that ElectionGuard uses for conducting an election. The key component is the `contests` collection, which applies placeholder selections to the `Manifest` contests """ manifest: InitVar[Manifest] = None geopolitical_units: List[GeopoliticalUnit] = field(init=False) contests: List[ContestDescriptionWithPlaceholders] = field(init=False) ballot_styles: List[BallotStyle] = field(init=False) manifest_hash: ElementModQ = field(init=False) def __post_init__(self, manifest: Manifest) -> None: object.__setattr__(self, "manifest_hash", manifest.crypto_hash()) object.__setattr__(self, "geopolitical_units", manifest.geopolitical_units) object.__setattr__(self, "ballot_styles", manifest.ballot_styles) object.__setattr__( self, "contests", self._generate_contests_with_placeholders(manifest) ) def contest_for( self, contest_id: str ) -> Optional[ContestDescriptionWithPlaceholders]: """ Get contest by id :param contest_id: Contest id :return: Contest description or none """ matching_contests = list( filter(lambda i: i.object_id == contest_id, self.contests) ) if any(matching_contests): return matching_contests[0] return None def get_ballot_style(self, ballot_style_id: str) -> BallotStyle: """ Get a ballot style for a specified ballot_style_id """ style = list( filter(lambda i: i.object_id == ballot_style_id, self.ballot_styles) )[0] return style def get_contests_for( self, ballot_style_id: str ) -> List[ContestDescriptionWithPlaceholders]: """ Get contests for a ballot style :param ballot_style_id: ballot style id :return: contest descriptions """ style = self.get_ballot_style(ballot_style_id) if style.geopolitical_unit_ids is None: return [] # pylint: disable=unnecessary-comprehension gp_unit_ids = [gp_unit_id for gp_unit_id in style.geopolitical_unit_ids] contests = list( filter(lambda i: i.electoral_district_id in gp_unit_ids, self.contests) ) return contests @staticmethod def _generate_contests_with_placeholders( manifest: Manifest, ) -> List[ContestDescriptionWithPlaceholders]: """ For each contest, append the `number_elected` number of placeholder selections to the end of the contest collection """ contests: List[ContestDescriptionWithPlaceholders] = [] for contest in manifest.contests: placeholder_selections = generate_placeholder_selections_from( contest, contest.number_elected ) contests.append( contest_description_with_placeholders_from( contest, placeholder_selections ) ) return contests def contest_description_with_placeholders_from( description: ContestDescription, placeholders: List[SelectionDescription] ) -> ContestDescriptionWithPlaceholders: """ Generates a placeholder selection description :param description: contest description :param placeholders: list of placeholder descriptions of selections :return: a SelectionDescription or None """ return ContestDescriptionWithPlaceholders( object_id=description.object_id, electoral_district_id=description.electoral_district_id, sequence_order=description.sequence_order, vote_variation=description.vote_variation, number_elected=description.number_elected, votes_allowed=description.votes_allowed, name=description.name, ballot_selections=description.ballot_selections, ballot_title=description.ballot_title, ballot_subtitle=description.ballot_subtitle, placeholder_selections=placeholders, ) def generate_placeholder_selection_from( contest: ContestDescription, use_sequence_id: Optional[int] = None ) -> Optional[SelectionDescription]: """ Generates a placeholder selection description that is unique so it can be hashed :param use_sequence_id: an optional integer unique to the contest identifying this selection's place in the contest :return: a SelectionDescription or None """ sequence_ids = [selection.sequence_order for selection in contest.ballot_selections] if use_sequence_id is None: # if no sequence order is specified, take the max use_sequence_id = max(*sequence_ids) + 1 elif use_sequence_id in sequence_ids: log_warning( f"mismatched placeholder selection {use_sequence_id} already exists" ) return None placeholder_object_id = f"{contest.object_id}-{use_sequence_id}" return SelectionDescription( f"{placeholder_object_id}-placeholder", use_sequence_id, f"{placeholder_object_id}-candidate", ) def generate_placeholder_selections_from( contest: ContestDescription, count: int ) -> List[SelectionDescription]: """ Generates the specified number of placeholder selections in ascending sequence order from the max selection sequence orderf :param contest: ContestDescription for input :param count: optionally specify a number of placeholders to generate :return: a collection of `SelectionDescription` objects, which may be empty """ max_sequence_order = max( selection.sequence_order for selection in contest.ballot_selections ) selections: List[SelectionDescription] = [] for i in range(count): sequence_order = max_sequence_order + 1 + i selections.append( get_optional(generate_placeholder_selection_from(contest, sequence_order)) ) return selections def get_i8n_value(name: InternationalizedText, lang: str, default_val: str) -> str: query = (t.value for t in name.text if t.language == lang) result = next(query, "") return default_val if result == "" else result ================================================ FILE: src/electionguard/nonces.py ================================================ # pylint: disable=too-many-ancestors from typing import Union, Sequence, List, overload from electionguard.group import ElementModQ, ElementModPOrQ from electionguard.hash import hash_elems class Nonces(Sequence[ElementModQ]): """ Creates a sequence of random elements in [0,Q), seeded from an initial element in [0,Q). If you start with the same seed, you'll get exactly the same sequence. Optional string or ElementModPOrQ "headers" can be included alongside the seed both at construction time and when asking for the next nonce. This is useful when specifying what a nonce is being used for, to avoid various kinds of subtle cryptographic attacks. The Nonces class is a Sequence. It can be iterated, or it can be treated as an array and indexed. Asking for a nonce is constant time, regardless of the index. """ def __init__(self, seed: ElementModQ, *headers: Union[str, ElementModPOrQ]) -> None: if len(headers) > 0: self.__seed: ElementModQ = hash_elems(seed, *headers) else: self.__seed = seed # https://github.com/python/mypy/issues/4108 @overload def __getitem__(self, index: int) -> ElementModQ: pass @overload def __getitem__(self, index: slice) -> List[ElementModQ]: pass def __getitem__( self, index: Union[slice, int] ) -> Union[ElementModQ, List[ElementModQ]]: if isinstance(index, int): return self.get_with_headers(index) if isinstance(index.stop, int): # Handling slices is a pain: https://stackoverflow.com/a/42731787 indices = range(index.start or 0, index.stop, index.step or 1) return [self[i] for i in indices] raise TypeError("Cannot take unbounded slice of Nonces") def __len__(self) -> int: raise TypeError("Nonces does not have finite length") def get_with_headers(self, item: int, *headers: str) -> ElementModQ: """ Gets an item from the sequence at any offset. Headers can be included to optionally help specify what a nonce is being used for. :param item: Index into the nonces. :param headers: Optional string headers. """ if item < 0: raise TypeError("Nonces do not support negative indices.") return hash_elems(self.__seed, item, *headers) ================================================ FILE: src/electionguard/proof.py ================================================ from enum import Enum from .utils import space_between_capitals class ProofUsage(Enum): """Usage case for proof""" Unknown = "Unknown" SecretValue = "Prove knowledge of secret value" SelectionLimit = "Prove value within selection's limit" SelectionValue = "Prove selection's value (0 or 1)" class Proof: """Base class for proofs with name and usage case""" name: str = "Proof" usage: ProofUsage = ProofUsage.Unknown def __init__(self) -> None: object.__setattr__( self, "name", space_between_capitals(self.__class__.__name__) ) ================================================ FILE: src/electionguard/py.typed ================================================ ================================================ FILE: src/electionguard/scheduler.py ================================================ # pylint: disable=consider-using-with from __future__ import annotations import traceback from typing import Any, Callable, Iterable, List, TypeVar from contextlib import AbstractContextManager from multiprocessing import Pool as ProcessPool from multiprocessing.dummy import Pool as ThreadPool from multiprocessing.pool import Pool from psutil import cpu_count from .logs import log_warning from .singleton import Singleton _T = TypeVar("_T") class Scheduler(Singleton, AbstractContextManager): """ Worker that wraps Multprocessing and allows for shared context or spawning processes. Implemented as a singleton to guarantee there is only one set of tread and process pools in use throughout the library. Also implements the [Context Manager Protocol](https://docs.python.org/3.8/library/stdtypes.html#typecontextmanager) """ __process_pool: Pool __thread_pool: Pool def __init__(self) -> None: super().__init__() self._open() def __enter__(self) -> Scheduler: self._open() return self def __exit__(self, exc_type: Any, exc_value: Any, exc_traceback: Any) -> None: self.close() def _open(self) -> None: """Open pools""" max_processes = cpu_count(logical=False) # Reserve one CPU for I/O bound tasks if max_processes > 2: max_processes = max_processes - 1 self.__process_pool = ProcessPool(max_processes) self.__thread_pool = ThreadPool(max_processes) def close(self) -> None: """Close pools""" self.__process_pool.close() self.__thread_pool.close() @staticmethod def cpu_count() -> int: """Get CPU count""" return int(cpu_count(logical=False)) def schedule( self, task: Callable, arguments: Iterable[Iterable[Any]], with_shared_resources: bool = False, ) -> List[_T]: """ Schedule tasks with list of arguments :param task: the callable task to execute :param arguments: the list of lists passed to the task using starmap :param with_shared_resources: flag to use threads instead of processes allowing resources to be shared. note when using the threadpool, execution is bound by default to the [global interpreter lock] (https://docs.python.org/3.8/glossary.html#term-global-interpreter-lock) """ if with_shared_resources: return self.safe_starmap(self.__thread_pool, task, arguments) return self.safe_starmap(self.__process_pool, task, arguments) @staticmethod def safe_starmap( pool: Pool, task: Callable, arguments: Iterable[Iterable[Any]] ) -> List[_T]: """Safe wrapper around starmap to ensure pool is open""" try: return pool.starmap(task, arguments) except ValueError as e: log_warning( f"safe_starmap({task}, {arguments}) exception ValueError({str(e)})" ) return [] except Exception: # pylint: disable=broad-except log_warning( f"safe_starmap({task}, {arguments}) failed with \n {traceback.format_exc()}" ) return [] @staticmethod def safe_map(pool: Pool, task: Callable, arguments: Iterable[Any]) -> List[_T]: """Safe wrapper around starmap to ensure pool is open""" try: return pool.map(task, arguments) except ValueError as e: log_warning(f"safe_map({task}, {arguments}) exception ValueError({str(e)})") return [] except Exception: # pylint: disable=broad-except log_warning( f"safe_starmap({task}, {arguments}) failed with \n {traceback.format_exc()}" ) return [] ================================================ FILE: src/electionguard/schnorr.py ================================================ from dataclasses import dataclass from .elgamal import ElGamalKeyPair, ElGamalPublicKey from .group import ( ElementModQ, ElementModP, g_pow_p, mult_p, pow_p, a_plus_bc_q, ) from .hash import hash_elems from .logs import log_warning from .proof import Proof, ProofUsage @dataclass class SchnorrProof(Proof): """ Representation of a Schnorr proof """ public_key: ElGamalPublicKey """k in the spec""" commitment: ElementModP """h in the spec""" challenge: ElementModQ """c in the spec""" response: ElementModQ """u in the spec""" usage: ProofUsage = ProofUsage.SecretValue def __post_init__(self) -> None: super().__init__() def is_valid(self) -> bool: """ Check validity of the `proof` for proving possession of the private key corresponding to `public_key`. :return: true if the transcript is valid, false if anything is wrong """ k = self.public_key h = self.commitment u = self.response valid_public_key = k.is_valid_residue() in_bounds_h = h.is_in_bounds() in_bounds_u = u.is_in_bounds() c = hash_elems(k, h) valid_challenge = c == self.challenge valid_proof = g_pow_p(u) == mult_p(h, pow_p(k, c)) success = ( valid_public_key and in_bounds_h and in_bounds_u and valid_challenge and valid_proof ) if not success: log_warning( "found an invalid Schnorr proof: %s", str( { "in_bounds_h": in_bounds_h, "in_bounds_u": in_bounds_u, "valid_public_key": valid_public_key, "valid_challenge": valid_challenge, "valid_proof": valid_proof, "proof": self, } ), ) return success def make_schnorr_proof(keypair: ElGamalKeyPair, r: ElementModQ) -> SchnorrProof: """ Given an ElGamal keypair and a nonce, generates a proof that the prover knows the secret key without revealing it. :param keypair: An ElGamal keypair. :param r: A random element in [0,Q). """ k = keypair.public_key h = g_pow_p(r) c = hash_elems(k, h) u = a_plus_bc_q(r, keypair.secret_key, c) return SchnorrProof(k, h, c, u) ================================================ FILE: src/electionguard/serialize.py ================================================ from datetime import datetime from io import TextIOWrapper import json import os from pathlib import Path from typing import Any, List, Type, TypeVar, Union from dacite import Config, from_dict from dateutil import parser from pydantic_core import to_jsonable_python from pydantic.v1.tools import schema_json_of from .big_integer import BigInteger from .ballot_box import BallotBoxState from .byte_padding import add_padding, remove_padding, DataSize from .group import ElementModP, ElementModQ from .manifest import ElectionType, ReportingUnitType, VoteVariationType, SpecVersion from .proof import ProofUsage from .utils import BYTE_ENCODING, ContestErrorType _T = TypeVar("_T") _file_extension = "json" _config = Config( cast=[ BigInteger, ContestErrorType, ElementModP, ElementModQ, ElectionType, BallotBoxState, ElectionType, ReportingUnitType, SpecVersion, VoteVariationType, ProofUsage, ], type_hooks={datetime: parser.parse}, ) def padded_encode(data: Any, size: DataSize = DataSize.Bytes_512) -> bytes: return add_padding(to_raw(data).encode(BYTE_ENCODING), size) def padded_decode( type_: Type[_T], padded_data: bytes, size: DataSize = DataSize.Bytes_512 ) -> _T: return from_raw(type_, remove_padding(padded_data, size)) def construct_path( target_file_name: str, target_path: str = "", target_file_extension: str = _file_extension, ) -> str: """Construct path from file name, path, and extension.""" target_file = f"{target_file_name}.{target_file_extension}" return os.path.join(target_path, target_file) def from_raw(type_: Type[_T], raw: Union[str, bytes]) -> _T: """Deserialize raw json string as type.""" return from_dict(type_, json.loads(raw), _config) def from_list_raw(type_: Type[_T], raw: Union[str, bytes]) -> List[_T]: """Deserialize raw json string as type.""" data = json.loads(raw) ls: List[_T] = [] for item in data: ls.append(from_dict(type_, item, _config)) return ls def to_raw(data: Any) -> str: """Serialize data to raw json format.""" return json.dumps(to_jsonable_python(data)) def from_file_wrapper(type_: Type[_T], file: TextIOWrapper) -> _T: """Deserialize json file as type.""" data = json.load(file) return from_dict(type_, data, _config) def from_file(type_: Type[_T], path: Union[str, Path]) -> _T: """Deserialize json file as type.""" with open(path, "r", encoding=BYTE_ENCODING) as json_file: data = json.load(json_file) return from_dict(type_, data, _config) def from_list_in_file(type_: Type[_T], path: Union[str, Path]) -> List[_T]: """Deserialize json file that has an array of certain type.""" with open(path, "r", encoding=BYTE_ENCODING) as json_file: data = json.load(json_file) ls: List[_T] = [] for item in data: ls.append(from_dict(type_, item, _config)) return ls def from_list_in_file_wrapper(type_: Type[_T], file: TextIOWrapper) -> List[_T]: """Deserialize json file that has an array of certain type.""" data = json.load(file) ls: List[_T] = [] for item in data: ls.append(from_dict(type_, item, _config)) return ls def to_file( data: Any, target_file_name: str, target_path: str = "", ) -> str: """Serialize object to JSON""" if not os.path.exists(target_path): os.makedirs(target_path) path = construct_path(target_file_name, target_path) with open( path, "w", encoding=BYTE_ENCODING, ) as outfile: json.dump(to_jsonable_python(data), outfile) return path def get_schema(_type: Any) -> str: """Get JSON Schema for type""" return schema_json_of(_type) ================================================ FILE: src/electionguard/singleton.py ================================================ from typing import Any class Singleton: """A Singleton Class""" __instance = None @staticmethod def get_instance() -> Any: """Get a static instance of the derived class.""" if Singleton.__instance is None: Singleton() return Singleton.__instance def __init__(self) -> None: if Singleton.__instance is None: Singleton.__instance = self # pylint: disable=unused-private-member ================================================ FILE: src/electionguard/tally.py ================================================ # pylint: disable=unnecessary-comprehension from dataclasses import dataclass, field from typing import Iterable, Optional, List, Dict, Set, Tuple, Any from collections.abc import Container, Sized from .ballot import ( BallotBoxState, CiphertextBallotSelection, SubmittedBallot, CiphertextSelection, ) from .data_store import DataStore from .ballot_validator import ballot_is_valid_for_election from .decryption_share import CiphertextDecryptionSelection from .election import CiphertextElectionContext from .election_object_base import ElectionObjectBase, OrderedObjectBase from .elgamal import ElGamalCiphertext, elgamal_add from .group import ElementModQ, ONE_MOD_P, ElementModP from .logs import log_warning from .manifest import InternalManifest from .scheduler import Scheduler from .type import BallotId, ContestId, SelectionId @dataclass class PlaintextTallySelection(ElectionObjectBase): """ A plaintext Tally Selection is a decrypted selection of a contest """ tally: int # g^tally or M in the spec value: ElementModP message: ElGamalCiphertext shares: List[CiphertextDecryptionSelection] @dataclass class CiphertextTallySelection(ElectionObjectBase, CiphertextSelection): """ a CiphertextTallySelection is a homomorphic accumulation of all of the CiphertextBallotSelection instances for a specific selection in an election. """ sequence_order: int """Order of the selection.""" description_hash: ElementModQ """ The SelectionDescription hash """ ciphertext: ElGamalCiphertext = field( default_factory=lambda: ElGamalCiphertext(ONE_MOD_P, ONE_MOD_P) ) """ The encrypted representation of the total of all ballots for this selection """ def elgamal_accumulate( self, elgamal_ciphertext: ElGamalCiphertext ) -> ElGamalCiphertext: """ Homomorphically add the specified value to the message """ new_value = elgamal_add(self.ciphertext, elgamal_ciphertext) self.ciphertext = new_value return self.ciphertext @dataclass class PlaintextTallyContest(ElectionObjectBase): """ A plaintext Tally Contest is a collection of plaintext selections """ selections: Dict[SelectionId, PlaintextTallySelection] @dataclass class CiphertextTallyContest(OrderedObjectBase): """ A CiphertextTallyContest is a container for associating a collection of CiphertextTallySelection to a specific ContestDescription """ description_hash: ElementModQ """ The ContestDescription hash """ selections: Dict[SelectionId, CiphertextTallySelection] """ A collection of CiphertextTallySelection mapped by SelectionDescription.object_id """ def accumulate_contest( self, contest_selections: List[CiphertextBallotSelection], scheduler: Optional[Scheduler] = None, ) -> bool: """ Accumulate the contest selections of an individual ballot into this tally """ if len(contest_selections) == 0: log_warning( f"accumulate cannot add missing selections for contest {self.object_id}" ) return False # Validate the input data by comparing the selection id's provided # to the valid selection id's for this tally contest selection_ids = { selection.object_id for selection in contest_selections if not selection.is_placeholder_selection } if any(set(self.selections).difference(selection_ids)): log_warning( f"accumulate cannot add mismatched selections for contest {self.object_id}" ) return False if scheduler is None: scheduler = Scheduler() # iterate through the tally selections and add the new value to the total results: List[Tuple[SelectionId, Optional[ElGamalCiphertext]]] = ( scheduler.schedule( self._accumulate_selections, [ (key, selection_tally, contest_selections) for (key, selection_tally) in self.selections.items() ], ) ) for key, ciphertext in results: if ciphertext is None: return False self.selections[key].ciphertext = ciphertext return True @staticmethod def _accumulate_selections( key: SelectionId, selection_tally: CiphertextTallySelection, contest_selections: List[CiphertextBallotSelection], ) -> Tuple[SelectionId, Optional[ElGamalCiphertext]]: use_selection = None for selection in contest_selections: if key == selection.object_id: use_selection = selection break # a selection on the ballot that is required was not found # this should never happen when using the `CiphertextTally` # but sanity check anyway if not use_selection: log_warning(f"add cannot accumulate for missing selection {key}") return key, None return key, selection_tally.elgamal_accumulate(use_selection.ciphertext) @dataclass class PlaintextTally(ElectionObjectBase): """ The plaintext representation of all contests in the election """ contests: Dict[ContestId, PlaintextTallyContest] @dataclass class PublishedCiphertextTally(ElectionObjectBase): """ A published version of the ciphertext tally """ contests: Dict[ContestId, CiphertextTallyContest] @dataclass class CiphertextTally(ElectionObjectBase, Container, Sized): """ A `CiphertextTally` accepts cast and spoiled ballots and accumulates a tally on the cast ballots """ _internal_manifest: InternalManifest _encryption: CiphertextElectionContext cast_ballot_ids: Set[BallotId] = field(default_factory=lambda: set()) """A local cache of ballots id's that have already been cast""" spoiled_ballot_ids: Set[BallotId] = field(default_factory=lambda: set()) contests: Dict[ContestId, CiphertextTallyContest] = field(init=False) """ A collection of each contest and selection in an election. Retains an encrypted representation of a tally for each selection """ def __post_init__(self) -> None: object.__setattr__( self, "contests", self._build_tally_collection(self._internal_manifest) ) def __len__(self) -> int: return len(self.cast_ballot_ids) + len(self.spoiled_ballot_ids) def __contains__(self, item: object) -> bool: if not isinstance(item, SubmittedBallot): return False if ( item.object_id in self.cast_ballot_ids or item.object_id in self.spoiled_ballot_ids ): return True return False def append( self, ballot: SubmittedBallot, should_validate: bool, scheduler: Optional[Scheduler] = None, ) -> bool: """ Append a ballot to the tally and recalculate the tally. """ if ballot.state == BallotBoxState.UNKNOWN: log_warning(f"append cannot add {ballot.object_id} with invalid state") return False if ballot in self: log_warning(f"append cannot add {ballot.object_id} that is already tallied") return False if not ballot_is_valid_for_election( ballot, self._internal_manifest, self._encryption, should_validate ): return False if ballot.state == BallotBoxState.CAST: return self._add_cast(ballot, scheduler) if ballot.state == BallotBoxState.SPOILED: return self._add_spoiled(ballot) log_warning(f"append cannot add {ballot.object_id}") return False def batch_append( self, ballots: Iterable[Tuple[Any, SubmittedBallot]], should_validate: bool, scheduler: Optional[Scheduler] = None, ) -> bool: """ Append a collection of Ballots to the tally and recalculate """ cast_ballot_selections: Dict[SelectionId, Dict[BallotId, ElGamalCiphertext]] = ( {} ) for ballot in ballots: # get the value of the dict ballot_value = ballot[1] if ballot_value not in self and ballot_is_valid_for_election( ballot_value, self._internal_manifest, self._encryption, should_validate ): if ballot_value.state == BallotBoxState.CAST: # collect the selections so they can can be accumulated in parallel for contest in ballot_value.contests: for selection in contest.ballot_selections: if selection.object_id not in cast_ballot_selections: cast_ballot_selections[selection.object_id] = {} cast_ballot_selections[selection.object_id][ ballot_value.object_id ] = selection.ciphertext # just append the spoiled ballots elif ballot_value.state == BallotBoxState.SPOILED: self._add_spoiled(ballot_value) # cache the cast ballot id's so they are not double counted if self._execute_accumulate(cast_ballot_selections, scheduler): for ballot in ballots: # get the value of the dict ballot_value = ballot[1] if ballot_value.state == BallotBoxState.CAST: self.cast_ballot_ids.add(ballot_value.object_id) return True return False def cast(self) -> int: """ Get a count of the cast ballots """ return len(self.cast_ballot_ids) def spoiled(self) -> int: """ Get a count of the spoiled ballots """ return len(self.spoiled_ballot_ids) def publish(self) -> PublishedCiphertextTally: return PublishedCiphertextTally(self.object_id, self.contests) @staticmethod def _accumulate( id: str, ballot_selections: Dict[BallotId, ElGamalCiphertext] ) -> Tuple[str, ElGamalCiphertext]: return ( id, elgamal_add(*[ciphertext for ciphertext in ballot_selections.values()]), ) def _add_cast( self, ballot: SubmittedBallot, scheduler: Optional[Scheduler] = None ) -> bool: """ Add a cast ballot to the tally, synchronously """ # iterate through the contests and elgamal add for contest in ballot.contests: # This should never happen since the ballot is validated against the election metadata # but it's possible the local dictionary was modified so we double check. if not contest.object_id in self.contests: log_warning( f"add cast missing contest in valid set {contest.object_id}" ) return False use_contest = self.contests[contest.object_id] if not use_contest.accumulate_contest(contest.ballot_selections, scheduler): return False self.contests[contest.object_id] = use_contest self.cast_ballot_ids.add(ballot.object_id) return True def _add_spoiled(self, ballot: SubmittedBallot) -> bool: """ Add a spoiled ballot """ self.spoiled_ballot_ids.add(ballot.object_id) return True @staticmethod def _build_tally_collection( internal_manifest: InternalManifest, ) -> Dict[ContestId, CiphertextTallyContest]: """ Build the object graph for the tally from the InternalManifest """ cast_collection: Dict[str, CiphertextTallyContest] = {} for contest in internal_manifest.contests: # build a collection of valid selections for the contest description # note: we explicitly ignore the Placeholder Selections. contest_selections: Dict[str, CiphertextTallySelection] = {} for selection in contest.ballot_selections: contest_selections[selection.object_id] = CiphertextTallySelection( selection.object_id, selection.sequence_order, selection.crypto_hash(), ) cast_collection[contest.object_id] = CiphertextTallyContest( contest.object_id, contest.sequence_order, contest.crypto_hash(), contest_selections, ) return cast_collection def _execute_accumulate( self, ciphertext_selections_by_selection_id: Dict[ str, Dict[BallotId, ElGamalCiphertext] ], scheduler: Optional[Scheduler] = None, ) -> bool: result_set: List[Tuple[SelectionId, ElGamalCiphertext]] if not scheduler: scheduler = Scheduler() result_set = scheduler.schedule( self._accumulate, [ (selection_id, selections) for ( selection_id, selections, ) in ciphertext_selections_by_selection_id.items() ], ) result_dict = { selection_id: ciphertext for (selection_id, ciphertext) in result_set } for contest in self.contests.values(): for selection_id, selection in contest.selections.items(): if selection_id in result_dict: selection.elgamal_accumulate(result_dict[selection_id]) return True def tally_ballot( ballot: SubmittedBallot, tally: CiphertextTally ) -> Optional[CiphertextTally]: """ Tally a ballot that is either Cast or Spoiled :return: The mutated CiphertextTally or None if there is an error """ if ballot.state == BallotBoxState.UNKNOWN: log_warning( f"tally ballots error tallying unknown state for ballot {ballot.object_id}" ) return None if tally.append(ballot, True): return tally return None def tally_ballots( store: DataStore, internal_manifest: InternalManifest, context: CiphertextElectionContext, ) -> Optional[CiphertextTally]: """ Tally all of the ballots in the ballot store. :return: a CiphertextTally or None if there is an error """ # TODO: ISSUE #14: unique Id for the tally tally: CiphertextTally = CiphertextTally( "election-results", internal_manifest, context ) if tally.batch_append(store, True): return tally return None ================================================ FILE: src/electionguard/type.py ================================================ BallotId = str ContestId = str GuardianId = str MediatorId = str VerifierId = str SelectionId = str ================================================ FILE: src/electionguard/utils.py ================================================ from datetime import datetime, timezone from enum import Enum from re import sub from typing import Callable, List, Optional, TypeVar, Literal from base64 import b16decode from .type import ContestId, SelectionId _T = TypeVar("_T") _U = TypeVar("_U") BYTE_ORDER: Literal["little", "big"] = "big" BYTE_ENCODING = "utf-8" class ContestErrorType(Enum): """Various errors that can occur on ballots contest after voting.""" Default = "default" NullVote = "nullvote" UnderVote = "undervote" OverVote = "overvote" class ContestException(Exception): """Generic contest error""" type: ContestErrorType def __init__( self, contest_id: ContestId, type: ContestErrorType = ContestErrorType.Default, override_message: Optional[str] = None, ): self.type = type super().__init__( override_message or f"{type} error has occurred on contest {contest_id}." ) class OverVoteException(ContestException): """Over vote on contest error.""" overvoted_ids: List[SelectionId] def __init__(self, contest_id: ContestId, overvoted_ids: List[SelectionId]): self.overvoted_ids = overvoted_ids super().__init__(contest_id, ContestErrorType.OverVote) class UnderVoteException(ContestException): """Under vote on contest error.""" def __init__(self, contest_id: ContestId): super().__init__(contest_id, ContestErrorType.UnderVote) class NullVoteException(ContestException): """Null vote on contest error.""" def __init__(self, contest_id: ContestId): super().__init__(contest_id, ContestErrorType.NullVote) def get_optional(optional: Optional[_T]) -> _T: """ General-purpose unwrapping function to handle `Optional`. Raises an exception if it's actually `None`, otherwise returns the internal type. """ assert optional is not None, "Unwrap called on None" return optional def match_optional( optional: Optional[_T], none_func: Callable[[], _U], some_func: Callable[[_T], _U] ) -> _U: """ General-purpose pattern-matching function to handle `Optional`. If it's actually `None`, the `none_func` lambda is called. Otherwise, the `some_func` lambda is called with the value. """ if optional is None: return none_func() return some_func(optional) def get_or_else_optional(optional: Optional[_T], alt_value: _T) -> _T: """ General-purpose getter for `Optional`. If it's `None`, returns the `alt_value`. Otherwise, returns the contents of `optional`. """ if optional is None: return alt_value return optional def get_or_else_optional_func(optional: Optional[_T], func: Callable[[], _T]) -> _T: """ General-purpose getter for `Optional`. If it's `None`, calls the lambda `func` and returns its value. Otherwise, returns the contents of `optional`. """ if optional is None: return func() return optional def flatmap_optional( optional: Optional[_T], mapper: Callable[[_T], _U] ) -> Optional[_U]: """ General-purpose flatmapping on `Optional`. If it's `None`, returns `None` as well, otherwise returns the lambda applied to the contents. """ if optional is None: return None return mapper(optional) def to_hex_bytes(data: bytes) -> bytes: """ Convert from the element to the representation of bytes by first going through hex. """ return b16decode(data) def to_ticks(date_time: datetime) -> int: """ Return the number of ticks for a date time. Ticks are defined here as number of seconds since the unix epoch (00:00:00 UTC on 1 January 1970) :param date_time: Date time to convert :return: number of ticks """ ticks = ( date_time.timestamp() if date_time.tzinfo else date_time.astimezone(timezone.utc).timestamp() ) return int(ticks) def to_iso_date_string(date_time: datetime) -> str: """ Return the number of ticks for a date time. Ticks are defined here as number of seconds since the unix epoch (00:00:00 UTC on 1 January 1970) :param date_time: Date time to convert :return: number of ticks """ utc_datetime = ( date_time.astimezone(timezone.utc).replace(microsecond=0) if date_time.tzinfo else date_time.replace(microsecond=0) ) return utc_datetime.strftime("%Y-%m-%dT%H:%M:%SZ") def space_between_capitals(base: str) -> str: """ Return a modified string with spaces between capital letters :param base: base string :return: modified string """ return sub(r"(\w)([A-Z])", r"\1 \2", base) ================================================ FILE: src/electionguard_cli/__init__.py ================================================ from electionguard_cli import cli_models from electionguard_cli import cli_steps from electionguard_cli import e2e from electionguard_cli import encrypt_ballots from electionguard_cli import import_ballots from electionguard_cli import mark_ballots from electionguard_cli import setup_election from electionguard_cli import start from electionguard_cli import submit_ballots from electionguard_cli.cli_models import ( BuildElectionResults, CliDecryptResults, CliElectionInputsBase, EncryptResults, MarkResults, SubmitResults, cli_decrypt_results, cli_election_inputs_base, e2e_build_election_results, encrypt_results, mark_results, submit_results, ) from electionguard_cli.cli_steps import ( CliStepBase, DecryptStep, ElectionBuilderStep, EncryptVotesStep, InputRetrievalStepBase, KeyCeremonyStep, MarkBallotsStep, OutputStepBase, PrintResultsStep, SubmitBallotsStep, TallyStep, cli_step_base, decrypt_step, election_builder_step, encrypt_votes_step, input_retrieval_step_base, key_ceremony_step, mark_ballots_step, output_step_base, print_results_step, submit_ballots_step, tally_step, ) from electionguard_cli.e2e import ( E2eCommand, E2eElectionBuilderStep, E2eInputRetrievalStep, E2eInputs, E2ePublishStep, SubmitVotesStep, e2e_command, e2e_election_builder_step, e2e_input_retrieval_step, e2e_inputs, e2e_publish_step, submit_votes_step, ) from electionguard_cli.encrypt_ballots import ( EncryptBallotInputs, EncryptBallotsCommand, EncryptBallotsElectionBuilderStep, EncryptBallotsInputRetrievalStep, EncryptBallotsPublishStep, encrypt_ballot_inputs, encrypt_ballots_election_builder_step, encrypt_ballots_input_retrieval_step, encrypt_ballots_publish_step, encrypt_command, ) from electionguard_cli.import_ballots import ( ImportBallotInputs, ImportBallotsCommand, ImportBallotsElectionBuilderStep, ImportBallotsInputRetrievalStep, ImportBallotsPublishStep, import_ballot_inputs, import_ballots_command, import_ballots_election_builder_step, import_ballots_input_retrieval_step, import_ballots_publish_step, ) from electionguard_cli.mark_ballots import ( MarkBallotInputs, MarkBallotsCommand, MarkBallotsElectionBuilderStep, MarkBallotsInputRetrievalStep, MarkBallotsPublishStep, mark_ballot_inputs, mark_ballots_election_builder_step, mark_ballots_input_retrieval_step, mark_ballots_publish_step, mark_command, ) from electionguard_cli.setup_election import ( OutputSetupFilesStep, SetupElectionBuilderStep, SetupElectionCommand, SetupInputRetrievalStep, SetupInputs, output_setup_files_step, setup_election_builder_step, setup_election_command, setup_input_retrieval_step, setup_inputs, ) from electionguard_cli.start import ( cli, ) from electionguard_cli.submit_ballots import ( SubmitBallotInputs, SubmitBallotsCommand, SubmitBallotsElectionBuilderStep, SubmitBallotsInputRetrievalStep, SubmitBallotsPublishStep, submit_ballot_inputs, submit_ballots_election_builder_step, submit_ballots_input_retrieval_step, submit_ballots_publish_step, submit_command, ) __all__ = [ "BuildElectionResults", "CliDecryptResults", "CliElectionInputsBase", "CliStepBase", "DecryptStep", "E2eCommand", "E2eElectionBuilderStep", "E2eInputRetrievalStep", "E2eInputs", "E2ePublishStep", "ElectionBuilderStep", "EncryptBallotInputs", "EncryptBallotsCommand", "EncryptBallotsElectionBuilderStep", "EncryptBallotsInputRetrievalStep", "EncryptBallotsPublishStep", "EncryptResults", "EncryptVotesStep", "ImportBallotInputs", "ImportBallotsCommand", "ImportBallotsElectionBuilderStep", "ImportBallotsInputRetrievalStep", "ImportBallotsPublishStep", "InputRetrievalStepBase", "KeyCeremonyStep", "MarkBallotInputs", "MarkBallotsCommand", "MarkBallotsElectionBuilderStep", "MarkBallotsInputRetrievalStep", "MarkBallotsPublishStep", "MarkBallotsStep", "MarkResults", "OutputSetupFilesStep", "OutputStepBase", "PrintResultsStep", "SetupElectionBuilderStep", "SetupElectionCommand", "SetupInputRetrievalStep", "SetupInputs", "SubmitBallotInputs", "SubmitBallotsCommand", "SubmitBallotsElectionBuilderStep", "SubmitBallotsInputRetrievalStep", "SubmitBallotsPublishStep", "SubmitBallotsStep", "SubmitResults", "SubmitVotesStep", "TallyStep", "cli", "cli_decrypt_results", "cli_election_inputs_base", "cli_models", "cli_step_base", "cli_steps", "decrypt_step", "e2e", "e2e_build_election_results", "e2e_command", "e2e_election_builder_step", "e2e_input_retrieval_step", "e2e_inputs", "e2e_publish_step", "election_builder_step", "encrypt_ballot_inputs", "encrypt_ballots", "encrypt_ballots_election_builder_step", "encrypt_ballots_input_retrieval_step", "encrypt_ballots_publish_step", "encrypt_command", "encrypt_results", "encrypt_votes_step", "import_ballot_inputs", "import_ballots", "import_ballots_command", "import_ballots_election_builder_step", "import_ballots_input_retrieval_step", "import_ballots_publish_step", "input_retrieval_step_base", "key_ceremony_step", "mark_ballot_inputs", "mark_ballots", "mark_ballots_election_builder_step", "mark_ballots_input_retrieval_step", "mark_ballots_publish_step", "mark_ballots_step", "mark_command", "mark_results", "output_setup_files_step", "output_step_base", "print_results_step", "setup_election", "setup_election_builder_step", "setup_election_command", "setup_input_retrieval_step", "setup_inputs", "start", "submit_ballot_inputs", "submit_ballots", "submit_ballots_election_builder_step", "submit_ballots_input_retrieval_step", "submit_ballots_publish_step", "submit_ballots_step", "submit_command", "submit_results", "submit_votes_step", "tally_step", ] ================================================ FILE: src/electionguard_cli/cli_models/__init__.py ================================================ from electionguard_cli.cli_models import cli_decrypt_results from electionguard_cli.cli_models import cli_election_inputs_base from electionguard_cli.cli_models import e2e_build_election_results from electionguard_cli.cli_models import encrypt_results from electionguard_cli.cli_models import mark_results from electionguard_cli.cli_models import submit_results from electionguard_cli.cli_models.cli_decrypt_results import ( CliDecryptResults, ) from electionguard_cli.cli_models.cli_election_inputs_base import ( CliElectionInputsBase, ) from electionguard_cli.cli_models.e2e_build_election_results import ( BuildElectionResults, ) from electionguard_cli.cli_models.encrypt_results import ( EncryptResults, ) from electionguard_cli.cli_models.mark_results import ( MarkResults, ) from electionguard_cli.cli_models.submit_results import ( SubmitResults, ) __all__ = [ "BuildElectionResults", "CliDecryptResults", "CliElectionInputsBase", "EncryptResults", "MarkResults", "SubmitResults", "cli_decrypt_results", "cli_election_inputs_base", "e2e_build_election_results", "encrypt_results", "mark_results", "submit_results", ] ================================================ FILE: src/electionguard_cli/cli_models/cli_decrypt_results.py ================================================ from dataclasses import dataclass from typing import Dict from electionguard.tally import CiphertextTally, PlaintextTally from electionguard.type import BallotId from electionguard.election_polynomial import LagrangeCoefficientsRecord @dataclass class CliDecryptResults: """Responsible for holding the results of decrypting an election.""" plaintext_tally: PlaintextTally plaintext_spoiled_ballots: Dict[BallotId, PlaintextTally] ciphertext_tally: CiphertextTally lagrange_coefficients: LagrangeCoefficientsRecord ================================================ FILE: src/electionguard_cli/cli_models/cli_election_inputs_base.py ================================================ from abc import ABC from typing import List from electionguard.guardian import Guardian from electionguard.manifest import Manifest class CliElectionInputsBase(ABC): """Responsible for holding inputs common to all CLI election commands""" guardian_count: int quorum: int manifest: Manifest guardians: List[Guardian] ================================================ FILE: src/electionguard_cli/cli_models/e2e_build_election_results.py ================================================ from dataclasses import dataclass from electionguard.election import CiphertextElectionContext from electionguard.manifest import InternalManifest @dataclass class BuildElectionResults: """The results of building an election, more specifically an internal manifest and context.""" internal_manifest: InternalManifest context: CiphertextElectionContext ================================================ FILE: src/electionguard_cli/cli_models/encrypt_results.py ================================================ from dataclasses import dataclass from typing import List from electionguard.ballot import CiphertextBallot from electionguard.encrypt import EncryptionDevice @dataclass class EncryptResults: """Responsible for holding the results of encrypting votes in an election.""" device: EncryptionDevice ciphertext_ballots: List[CiphertextBallot] ================================================ FILE: src/electionguard_cli/cli_models/mark_results.py ================================================ from typing import List from electionguard.ballot import PlaintextBallot class MarkResults: """Responsible for holding the results of marking ballots in an election.""" def __init__( self, plaintext_ballots: List[PlaintextBallot], ): self.plaintext_ballots = plaintext_ballots plaintext_ballots: List[PlaintextBallot] ================================================ FILE: src/electionguard_cli/cli_models/submit_results.py ================================================ from typing import List from electionguard.ballot import SubmittedBallot class SubmitResults: """Responsible for holding the results of submitting ballots in an election.""" def __init__( self, submitted_ballots: List[SubmittedBallot], ): self.submitted_ballots = submitted_ballots submitted_ballots: List[SubmittedBallot] ================================================ FILE: src/electionguard_cli/cli_steps/__init__.py ================================================ from electionguard_cli.cli_steps import cli_step_base from electionguard_cli.cli_steps import decrypt_step from electionguard_cli.cli_steps import election_builder_step from electionguard_cli.cli_steps import encrypt_votes_step from electionguard_cli.cli_steps import input_retrieval_step_base from electionguard_cli.cli_steps import key_ceremony_step from electionguard_cli.cli_steps import mark_ballots_step from electionguard_cli.cli_steps import output_step_base from electionguard_cli.cli_steps import print_results_step from electionguard_cli.cli_steps import submit_ballots_step from electionguard_cli.cli_steps import tally_step from electionguard_cli.cli_steps.cli_step_base import ( CliStepBase, ) from electionguard_cli.cli_steps.decrypt_step import ( DecryptStep, ) from electionguard_cli.cli_steps.election_builder_step import ( ElectionBuilderStep, ) from electionguard_cli.cli_steps.encrypt_votes_step import ( EncryptVotesStep, ) from electionguard_cli.cli_steps.input_retrieval_step_base import ( InputRetrievalStepBase, ) from electionguard_cli.cli_steps.key_ceremony_step import ( KeyCeremonyStep, ) from electionguard_cli.cli_steps.mark_ballots_step import ( MarkBallotsStep, ) from electionguard_cli.cli_steps.output_step_base import ( OutputStepBase, ) from electionguard_cli.cli_steps.print_results_step import ( PrintResultsStep, ) from electionguard_cli.cli_steps.submit_ballots_step import ( SubmitBallotsStep, ) from electionguard_cli.cli_steps.tally_step import ( TallyStep, ) __all__ = [ "CliStepBase", "DecryptStep", "ElectionBuilderStep", "EncryptVotesStep", "InputRetrievalStepBase", "KeyCeremonyStep", "MarkBallotsStep", "OutputStepBase", "PrintResultsStep", "SubmitBallotsStep", "TallyStep", "cli_step_base", "decrypt_step", "election_builder_step", "encrypt_votes_step", "input_retrieval_step_base", "key_ceremony_step", "mark_ballots_step", "output_step_base", "print_results_step", "submit_ballots_step", "tally_step", ] ================================================ FILE: src/electionguard_cli/cli_steps/cli_step_base.py ================================================ from typing import Any, Optional import click class CliStepBase: """ Responsible for providing common functionality to the individual steps within an end-to-end election command from the CLI. """ header_color = "green" value_color = "yellow" warning_color = "bright_red" section_color = "bright_white" VERIFICATION_URL_NAME = "verification_url" def print_header(self, s: str) -> None: click.echo("") click.secho(f"{'-'*40}", fg=self.header_color) click.secho(s, fg=self.header_color) click.secho(f"{'-'*40}", fg=self.header_color) def print_section(self, s: Optional[str]) -> None: click.echo("") click.secho(s, fg=self.section_color, bold=True) def print_value(self, name: str, value: Any) -> None: click.echo(click.style(name + ": ") + click.style(value, fg=self.value_color)) def print_warning(self, s: str) -> None: click.secho(f"WARNING: {s}", fg=self.warning_color) ================================================ FILE: src/electionguard_cli/cli_steps/decrypt_step.py ================================================ from typing import List import click from electionguard.guardian import Guardian from electionguard.utils import get_optional from electionguard.ballot import SubmittedBallot from electionguard.manifest import Manifest from electionguard.tally import CiphertextTally from electionguard.decryption_mediator import DecryptionMediator from electionguard.election_polynomial import LagrangeCoefficientsRecord from ..cli_models import BuildElectionResults, CliDecryptResults from .cli_step_base import CliStepBase class DecryptStep(CliStepBase): """Responsible for decrypting a tally and/or cast ballots""" def _get_lagrange_coefficients( self, decryption_mediator: DecryptionMediator ) -> LagrangeCoefficientsRecord: lagrange_coefficients = LagrangeCoefficientsRecord( decryption_mediator.get_lagrange_coefficients() ) coefficient_count = len(lagrange_coefficients.coefficients) self.print_value("Lagrange coefficients retrieved", coefficient_count) return lagrange_coefficients def decrypt( self, ciphertext_tally: CiphertextTally, spoiled_ballots: List[SubmittedBallot], guardians: List[Guardian], build_election_results: BuildElectionResults, manifest: Manifest, ) -> CliDecryptResults: self.print_header("Decrypting tally") decryption_mediator = DecryptionMediator( "decryption-mediator", build_election_results.context, ) context = build_election_results.context self.print_value("Cast ballots", ciphertext_tally.cast()) self.print_value("Spoiled ballots", ciphertext_tally.spoiled()) self.print_value("Total ballots", len(ciphertext_tally)) count = 0 for guardian in guardians: guardian_key = guardian.share_key() tally_share = get_optional( guardian.compute_tally_share(ciphertext_tally, context) ) ballot_shares = guardian.compute_ballot_shares(spoiled_ballots, context) decryption_mediator.announce(guardian_key, tally_share, ballot_shares) count += 1 click.echo(f"Guardian Present: {guardian.id}") lagrange_coefficients = self._get_lagrange_coefficients(decryption_mediator) plaintext_tally = get_optional( decryption_mediator.get_plaintext_tally(ciphertext_tally, manifest) ) plaintext_spoiled_ballots = get_optional( decryption_mediator.get_plaintext_ballots(spoiled_ballots, manifest) ) return CliDecryptResults( plaintext_tally, plaintext_spoiled_ballots, ciphertext_tally, lagrange_coefficients, ) ================================================ FILE: src/electionguard_cli/cli_steps/election_builder_step.py ================================================ from typing import Optional import click from electionguard.elgamal import ElGamalPublicKey from electionguard.group import ElementModQ from electionguard.utils import get_optional from electionguard_tools.helpers.election_builder import ElectionBuilder from ..cli_models import CliElectionInputsBase, BuildElectionResults from .cli_step_base import CliStepBase class ElectionBuilderStep(CliStepBase): """Responsible for creating a manifest and context for use in an election.""" def _build_election( self, election_inputs: CliElectionInputsBase, joint_public_key: ElGamalPublicKey, committment_hash: ElementModQ, verification_url: Optional[str], ) -> BuildElectionResults: self.print_header("Building election") click.echo("Initializing public key and commitment hash") election_builder = ElectionBuilder( election_inputs.guardian_count, election_inputs.quorum, election_inputs.manifest, ) election_builder.set_public_key(joint_public_key) election_builder.set_commitment_hash(committment_hash) if verification_url is not None: election_builder.add_extended_data_field( self.VERIFICATION_URL_NAME, verification_url ) click.echo("Creating context and internal manifest") build_result = election_builder.build() internal_manifest, context = get_optional(build_result) return BuildElectionResults(internal_manifest, context) ================================================ FILE: src/electionguard_cli/cli_steps/encrypt_votes_step.py ================================================ from typing import List, Tuple import click from electionguard.encrypt import EncryptionDevice, EncryptionMediator from electionguard.election import CiphertextElectionContext from electionguard.manifest import InternalManifest from electionguard.utils import get_optional from electionguard.ballot import ( CiphertextBallot, PlaintextBallot, ) from electionguard_tools.factories import ( ElectionFactory, ) from .cli_step_base import CliStepBase from ..cli_models import BuildElectionResults, EncryptResults class EncryptVotesStep(CliStepBase): """Responsible for encrypting votes and storing them in a ballot store.""" def encrypt( self, ballots: List[PlaintextBallot], build_election_results: BuildElectionResults, ) -> EncryptResults: self.print_header("Encrypting Ballots") internal_manifest = build_election_results.internal_manifest context = build_election_results.context (ciphertext_ballots, device) = self._encrypt_votes( ballots, internal_manifest, context ) return EncryptResults(device, ciphertext_ballots) def _get_encrypter( self, internal_manifest: InternalManifest, context: CiphertextElectionContext, ) -> Tuple[EncryptionMediator, EncryptionDevice]: device = ElectionFactory.get_encryption_device() self.print_value("Device location", device.location) encrypter = EncryptionMediator(internal_manifest, context, device) return (encrypter, device) def _encrypt_votes( self, plaintext_ballots: List[PlaintextBallot], internal_manifest: InternalManifest, context: CiphertextElectionContext, ) -> Tuple[List[CiphertextBallot], EncryptionDevice]: self.print_value("Ballots to encrypt", len(plaintext_ballots)) (encrypter, device) = self._get_encrypter(internal_manifest, context) encrypted_ballots = EncryptVotesStep._encrypt_ballots( plaintext_ballots, encrypter ) return (encrypted_ballots, device) @staticmethod def _encrypt_ballots( plaintext_ballots: List[PlaintextBallot], encrypter: EncryptionMediator ) -> List[CiphertextBallot]: ciphertext_ballots: List[CiphertextBallot] = [] for plaintext_ballot in plaintext_ballots: click.echo(f"Encrypting ballot: {plaintext_ballot.object_id}") encrypted_ballot = encrypter.encrypt(plaintext_ballot) ciphertext_ballots.append(get_optional(encrypted_ballot)) return ciphertext_ballots ================================================ FILE: src/electionguard_cli/cli_steps/input_retrieval_step_base.py ================================================ from typing import List, Type, TypeVar from os.path import isfile, isdir, join from os import listdir from io import TextIOWrapper from click import echo from electionguard.election import CiphertextElectionContext from electionguard.manifest import Manifest from electionguard.serialize import from_list_in_file, from_file, from_raw from electionguard.serialize import ( from_file_wrapper, ) from .cli_step_base import CliStepBase _T = TypeVar("_T") class InputRetrievalStepBase(CliStepBase): """A common base class for all CLI commands that accept user input""" def _get_manifest(self, manifest_file: TextIOWrapper) -> Manifest: manifest: Manifest = from_file_wrapper(Manifest, manifest_file) if not manifest.is_valid(): raise ValueError("manifest file is invalid") self.__print_manifest(manifest) return manifest def _get_manifest_raw(self, manifest_raw: str) -> Manifest: manifest: Manifest = from_raw(Manifest, manifest_raw) if not manifest.is_valid(): raise ValueError("manifest file is invalid") self.__print_manifest(manifest) return manifest def __print_manifest(self, manifest: Manifest) -> None: self.print_value("Name", manifest.get_name()) self.print_value("Scope", manifest.election_scope_id) self.print_value("Geopolitical Units", len(manifest.geopolitical_units)) self.print_value("Parties", len(manifest.parties)) self.print_value("Candidates", len(manifest.candidates)) self.print_value("Contests", len(manifest.contests)) self.print_value("Ballot Styles", len(manifest.ballot_styles)) @staticmethod def _get_context(context_file: TextIOWrapper) -> CiphertextElectionContext: return from_file_wrapper(CiphertextElectionContext, context_file) @staticmethod def _get_ballots(ballots_path: str, ballot_type: Type[_T]) -> List[_T]: if isfile(ballots_path): return from_list_in_file(ballot_type, ballots_path) if isdir(ballots_path): files = listdir(ballots_path) return [ InputRetrievalStepBase._get_ballot(ballots_path, f, ballot_type) for f in files ] raise ValueError( f"{ballots_path} is neither a valid file nor a valid directory" ) @staticmethod def _get_ballot(ballots_dir: str, filename: str, ballot_type: Type[_T]) -> _T: full_file = join(ballots_dir, filename) echo(f"Importing {filename}") return from_file(ballot_type, full_file) ================================================ FILE: src/electionguard_cli/cli_steps/key_ceremony_step.py ================================================ from typing import List from electionguard.guardian import Guardian from electionguard.key_ceremony import ( ElectionJointKey, ) from electionguard.key_ceremony_mediator import KeyCeremonyMediator from electionguard.utils import get_optional from electionguard_tools.helpers.key_ceremony_orchestrator import ( KeyCeremonyOrchestrator, ) from .cli_step_base import CliStepBase class KeyCeremonyStep(CliStepBase): """Responsible for running a key ceremony and producing an elgamal public key given a list of guardians.""" def run_key_ceremony(self, guardians: List[Guardian]) -> ElectionJointKey: self.print_header("Performing key ceremony") mediator: KeyCeremonyMediator = KeyCeremonyMediator( "mediator_1", guardians[0].ceremony_details ) KeyCeremonyOrchestrator.perform_full_ceremony(guardians, mediator) joint_key = mediator.publish_joint_key() self.print_value("Joint Key", get_optional(joint_key).joint_public_key) return get_optional(joint_key) ================================================ FILE: src/electionguard_cli/cli_steps/mark_ballots_step.py ================================================ from typing import Optional from electionguard_tools.factories import BallotFactory from .cli_step_base import CliStepBase from ..cli_models import BuildElectionResults, MarkResults class MarkBallotsStep(CliStepBase): """Responsible for marking ballots.""" def mark( self, build_election_results: BuildElectionResults, num_ballots: int, ballot_style_id: Optional[str], ) -> MarkResults: self.print_header("Marking Ballots") internal_manifest = build_election_results.internal_manifest ballot_factory = BallotFactory() plaintext_ballots = ballot_factory.generate_fake_plaintext_ballots_for_election( internal_manifest, num_ballots, ballot_style_id, allow_null_votes=False, allow_under_votes=False, ) return MarkResults(plaintext_ballots) ================================================ FILE: src/electionguard_cli/cli_steps/output_step_base.py ================================================ from typing import List, Any from electionguard import to_file from electionguard.guardian import Guardian, GuardianRecord from electionguard_tools.helpers.export import GUARDIAN_PREFIX from .cli_step_base import CliStepBase from ..cli_models import CliElectionInputsBase class OutputStepBase(CliStepBase): """Responsible for common functionality across all CLI commands related to outputting results.""" _COMPRESSION_FORMAT = "zip" def _export_private_keys(self, output_keys: str, guardians: List[Guardian]) -> None: if output_keys is None: return private_guardian_records = [ guardian.export_private_data() for guardian in guardians ] file_path = output_keys for private_guardian_record in private_guardian_records: file_name = GUARDIAN_PREFIX + private_guardian_record.guardian_id to_file(private_guardian_record, file_name, file_path) self.print_value("Guardian private keys", output_keys) self.print_warning( f"The files in {file_path} are secret and should be protected securely and not shared." ) @staticmethod def _get_guardian_records( election_inputs: CliElectionInputsBase, ) -> List[GuardianRecord]: return [guardian.publish() for guardian in election_inputs.guardians] def _export_file( self, title: str, content: Any, file_dir: str, file_name: str, ) -> str: location = to_file(content, file_name, file_dir) self.print_value(title, location) return location ================================================ FILE: src/electionguard_cli/cli_steps/print_results_step.py ================================================ from typing import Dict from electionguard.manifest import Manifest from electionguard.type import BallotId from electionguard.tally import ( PlaintextTally, ) from ..cli_models import CliDecryptResults from .cli_step_base import CliStepBase class PrintResultsStep(CliStepBase): """Responsible for printing the results of an end-to-end election.""" def _print_tally( self, plaintext_tally: PlaintextTally, contest_names: Dict[str, str], selection_names: Dict[str, str], ) -> None: self.print_header("Decrypted tally") for tally_contest in plaintext_tally.contests.values(): contest_name = contest_names.get(tally_contest.object_id) self.print_section(contest_name) for selection in tally_contest.selections.values(): name = selection_names[selection.object_id] self.print_value(f" {name}", selection.tally) def _print_spoiled_ballots( self, plaintext_spoiled_ballots: Dict[BallotId, PlaintextTally], contest_names: Dict[str, str], selection_names: Dict[str, str], ) -> None: ballot_ids = plaintext_spoiled_ballots.keys() for ballot_id in ballot_ids: self.print_header(f"Spoiled ballot '{ballot_id}'") spoiled_ballot = plaintext_spoiled_ballots[ballot_id] for contest in spoiled_ballot.contests.values(): contest_name = contest_names.get(contest.object_id) self.print_section(contest_name) for selection in contest.selections.values(): name = selection_names[selection.object_id] self.print_value(f" {name}", selection.tally) def print_election_results( self, decrypt_results: CliDecryptResults, manifest: Manifest ) -> None: selection_names = manifest.get_selection_names("en") contest_names = manifest.get_contest_names() self._print_tally( decrypt_results.plaintext_tally, contest_names, selection_names ) self._print_spoiled_ballots( decrypt_results.plaintext_spoiled_ballots, contest_names, selection_names ) ================================================ FILE: src/electionguard_cli/cli_steps/submit_ballots_step.py ================================================ from typing import List import click from electionguard.data_store import DataStore from electionguard.ballot_box import BallotBox from electionguard.ballot import CiphertextBallot from .cli_step_base import CliStepBase from ..cli_models import BuildElectionResults, SubmitResults class SubmitBallotsStep(CliStepBase): """Responsible for submitting ballots.""" def submit( self, build_election_results: BuildElectionResults, cast_ballots: List[CiphertextBallot], spoil_ballots: List[CiphertextBallot], ) -> SubmitResults: self.print_header("Submitting Ballots") internal_manifest = build_election_results.internal_manifest context = build_election_results.context ballot_store: DataStore = DataStore() ballot_box = BallotBox(internal_manifest, context, ballot_store) for ballot in cast_ballots: ballot_box.cast(ballot) click.echo(f"Cast Ballot Id: {ballot.object_id}") for ballot in spoil_ballots: ballot_box.spoil(ballot) click.echo(f"Spoilt Ballot Id: {ballot.object_id}") return SubmitResults(ballot_store.all()) ================================================ FILE: src/electionguard_cli/cli_steps/tally_step.py ================================================ from typing import Iterable, List, Tuple from electionguard.scheduler import Scheduler from electionguard.data_store import DataStore from electionguard.tally import CiphertextTally from electionguard.ballot_box import get_ballots from electionguard.ballot import BallotBoxState, SubmittedBallot from ..cli_models import BuildElectionResults from .cli_step_base import CliStepBase class TallyStep(CliStepBase): """Responsible for creating a tally and retrieving spoiled ballots.""" def get_from_ballots( self, build_election_results: BuildElectionResults, ballots: List[SubmittedBallot], ) -> Tuple[CiphertextTally, List[SubmittedBallot]]: self.print_header("Creating Tally") tuble_ballots = [(None, b) for b in ballots] tally = self._get_tally(build_election_results, tuble_ballots) spoiled_ballots = [b for b in ballots if b.state == BallotBoxState.SPOILED] return (tally, spoiled_ballots) def get_from_ballot_store( self, build_election_results: BuildElectionResults, ballot_store: DataStore ) -> Tuple[CiphertextTally, List[SubmittedBallot]]: self.print_header("Creating Tally") ciphertext_tally = self._get_tally(build_election_results, ballot_store.items()) spoiled_ballots = self._get_spoiled_ballots(ballot_store) return (ciphertext_tally, spoiled_ballots) def _get_tally( self, build_election_results: BuildElectionResults, ballots: Iterable[Tuple[None, SubmittedBallot]], ) -> CiphertextTally: tally = CiphertextTally( "election-results", build_election_results.internal_manifest, build_election_results.context, ) with Scheduler() as scheduler: tally.batch_append(ballots, True, scheduler) self.print_value("Ballots in tally", len(tally)) return tally def _get_spoiled_ballots(self, ballot_store: DataStore) -> List[SubmittedBallot]: submitted_ballots = get_ballots(ballot_store, BallotBoxState.SPOILED) spoiled_ballots_list = list(submitted_ballots.values()) self.print_value("Spoiled ballots", len(spoiled_ballots_list)) return spoiled_ballots_list ================================================ FILE: src/electionguard_cli/e2e/__init__.py ================================================ from electionguard_cli.e2e import e2e_command from electionguard_cli.e2e import e2e_election_builder_step from electionguard_cli.e2e import e2e_input_retrieval_step from electionguard_cli.e2e import e2e_inputs from electionguard_cli.e2e import e2e_publish_step from electionguard_cli.e2e import submit_votes_step from electionguard_cli.e2e.e2e_command import ( E2eCommand, ) from electionguard_cli.e2e.e2e_election_builder_step import ( E2eElectionBuilderStep, ) from electionguard_cli.e2e.e2e_input_retrieval_step import ( E2eInputRetrievalStep, ) from electionguard_cli.e2e.e2e_inputs import ( E2eInputs, ) from electionguard_cli.e2e.e2e_publish_step import ( E2ePublishStep, ) from electionguard_cli.e2e.submit_votes_step import ( SubmitVotesStep, ) __all__ = [ "E2eCommand", "E2eElectionBuilderStep", "E2eInputRetrievalStep", "E2eInputs", "E2ePublishStep", "SubmitVotesStep", "e2e_command", "e2e_election_builder_step", "e2e_input_retrieval_step", "e2e_inputs", "e2e_publish_step", "submit_votes_step", ] ================================================ FILE: src/electionguard_cli/e2e/e2e_command.py ================================================ from io import TextIOWrapper import click from ..cli_steps import ( DecryptStep, PrintResultsStep, TallyStep, KeyCeremonyStep, EncryptVotesStep, ) from .e2e_input_retrieval_step import E2eInputRetrievalStep from .submit_votes_step import SubmitVotesStep from .e2e_publish_step import E2ePublishStep from .e2e_election_builder_step import E2eElectionBuilderStep @click.command("e2e") @click.option( "--guardian-count", prompt="Number of guardians", help="The number of guardians that will participate in the key ceremony and tally.", type=click.INT, ) @click.option( "--quorum", prompt="Quorum", help="The minimum number of guardians required to show up to the tally.", type=click.INT, ) @click.option( "--manifest", prompt="Manifest file", help="The location of an election manifest.", type=click.File(), ) @click.option( "--ballots", prompt="Ballots file or directory", help="The location of a file or directory that contains plaintext ballots.", type=click.Path(exists=True, dir_okay=True, file_okay=True), ) @click.option( "--spoil-id", prompt="Object-id of ballot to spoil", help="The object-id of a ballot within the ballots file to spoil.", type=click.STRING, default=None, prompt_required=False, ) @click.option( "--url", help="An optional verification url for the election.", required=False, type=click.STRING, default=None, prompt=False, ) @click.option( "--output-record", help="A file name for saving an output election record (e.g. './election.zip')." + " If no value provided then an election record will not be generated.", type=click.Path( exists=False, dir_okay=False, file_okay=True, ), default=None, ) @click.option( "--output-keys", help="A directory for saving the private and public guardian keys (e.g. './guardian-keys')." + " If no value provided then no keys will be output.", type=click.Path(exists=False, dir_okay=True, file_okay=False, resolve_path=True), default=None, ) def E2eCommand( guardian_count: int, quorum: int, manifest: TextIOWrapper, ballots: str, spoil_id: str, url: str, output_record: str, output_keys: str, ) -> None: """Runs through an end-to-end election.""" # get user inputs election_inputs = E2eInputRetrievalStep().get_inputs( guardian_count, quorum, manifest, ballots, spoil_id, output_record, output_keys, url, ) # perform election joint_key = KeyCeremonyStep().run_key_ceremony(election_inputs.guardians) build_election_results = E2eElectionBuilderStep().build_election_with_key( election_inputs, joint_key ) encrypt_results = EncryptVotesStep().encrypt( election_inputs.ballots, build_election_results ) data_store = SubmitVotesStep().submit( election_inputs, build_election_results, encrypt_results ) (ciphertext_tally, spoiled_ballots) = TallyStep().get_from_ballot_store( build_election_results, data_store ) decrypt_results = DecryptStep().decrypt( ciphertext_tally, spoiled_ballots, election_inputs.guardians, build_election_results, election_inputs.manifest, ) # print results PrintResultsStep().print_election_results(decrypt_results, election_inputs.manifest) # publish election record E2ePublishStep().export( election_inputs, build_election_results, encrypt_results, decrypt_results, data_store, ) ================================================ FILE: src/electionguard_cli/e2e/e2e_election_builder_step.py ================================================ from electionguard.key_ceremony import ElectionJointKey from .e2e_inputs import E2eInputs from ..cli_models import BuildElectionResults from ..cli_steps import ElectionBuilderStep class E2eElectionBuilderStep(ElectionBuilderStep): """Responsible for creating a manifest and context for use in an election""" def build_election_with_key( self, election_inputs: E2eInputs, joint_key: ElectionJointKey ) -> BuildElectionResults: return self._build_election( election_inputs, joint_key.joint_public_key, joint_key.commitment_hash, election_inputs.verification_url, ) ================================================ FILE: src/electionguard_cli/e2e/e2e_input_retrieval_step.py ================================================ from io import TextIOWrapper from typing import Optional from electionguard.ballot import PlaintextBallot from electionguard.key_ceremony import CeremonyDetails from electionguard.manifest import Manifest from electionguard_tools.helpers.key_ceremony_orchestrator import ( KeyCeremonyOrchestrator, ) from ..cli_steps import ( InputRetrievalStepBase, ) from .e2e_inputs import E2eInputs class E2eInputRetrievalStep(InputRetrievalStepBase): """Responsible for retrieving and parsing user provided inputs for the CLI's e2e command.""" def get_inputs( self, guardian_count: int, quorum: int, manifest_file: TextIOWrapper, ballots_path: str, spoil_id: str, output_record: str, output_keys: str, verification_url: Optional[str], ) -> E2eInputs: self.print_header("Retrieving Inputs") guardians = KeyCeremonyOrchestrator.create_guardians( CeremonyDetails(guardian_count, quorum) ) manifest: Manifest = self._get_manifest(manifest_file) ballots = E2eInputRetrievalStep._get_ballots(ballots_path, PlaintextBallot) self.print_value("Guardians", guardian_count) self.print_value("Quorum", quorum) return E2eInputs( guardian_count, quorum, guardians, manifest, ballots, spoil_id, output_record, output_keys, verification_url, ) ================================================ FILE: src/electionguard_cli/e2e/e2e_inputs.py ================================================ from typing import List, Optional from electionguard.ballot import PlaintextBallot from electionguard.guardian import Guardian from electionguard.manifest import Manifest from ..cli_models import ( CliElectionInputsBase, ) # pylint: disable=too-many-instance-attributes class E2eInputs(CliElectionInputsBase): """Responsible for holding the inputs for the CLI's e2e command.""" def __init__( self, guardian_count: int, quorum: int, guardians: List[Guardian], manifest: Manifest, ballots: List[PlaintextBallot], spoil_id: str, output_record: str, output_keys: str, verification_url: Optional[str], ): self.guardian_count = guardian_count self.quorum = quorum self.guardians = guardians self.manifest = manifest self.ballots = ballots self.spoil_id = spoil_id self.output_record = output_record self.output_keys = output_keys self.verification_url = verification_url ballots: List[PlaintextBallot] spoil_id: str output_record: str output_keys: str verification_url: Optional[str] ================================================ FILE: src/electionguard_cli/e2e/e2e_publish_step.py ================================================ from shutil import make_archive from os.path import splitext from tempfile import TemporaryDirectory from click import echo from electionguard.constants import get_constants from electionguard.data_store import DataStore from electionguard_tools.helpers.export import export_record from .e2e_inputs import E2eInputs from ..cli_models import BuildElectionResults, CliDecryptResults, EncryptResults from ..cli_steps import OutputStepBase class E2ePublishStep(OutputStepBase): """Responsible for publishing an election record after an election has completed.""" def export( self, election_inputs: E2eInputs, build_election_results: BuildElectionResults, submit_results: EncryptResults, decrypt_results: CliDecryptResults, data_store: DataStore, ) -> None: self.print_header("Election Record") self._export_election_record( election_inputs, build_election_results, submit_results, decrypt_results, data_store, ) self._export_private_keys_e2e(election_inputs) def _export_election_record( self, election_inputs: E2eInputs, build_election_results: BuildElectionResults, encrypt_results: EncryptResults, decrypt_results: CliDecryptResults, data_store: DataStore, ) -> None: guardian_records = OutputStepBase._get_guardian_records(election_inputs) constants = get_constants() with TemporaryDirectory() as temp_dir: export_record( election_inputs.manifest, build_election_results.context, constants, [encrypt_results.device], data_store.all(), decrypt_results.plaintext_spoiled_ballots.values(), decrypt_results.ciphertext_tally.publish(), decrypt_results.plaintext_tally, guardian_records, decrypt_results.lagrange_coefficients, election_record_directory=temp_dir, ) file_name = splitext(election_inputs.output_record)[0] make_archive(file_name, self._COMPRESSION_FORMAT, temp_dir) echo(f"Exported election record to '{election_inputs.output_record}'") def _export_private_keys_e2e(self, election_inputs: E2eInputs) -> None: self._export_private_keys( election_inputs.output_keys, election_inputs.guardians ) ================================================ FILE: src/electionguard_cli/e2e/submit_votes_step.py ================================================ from typing import List import click from electionguard.data_store import DataStore from electionguard.ballot_box import BallotBox from electionguard.election import CiphertextElectionContext from electionguard.manifest import InternalManifest from electionguard.utils import get_optional from electionguard.ballot import ( CiphertextBallot, ) from ..cli_models import BuildElectionResults, EncryptResults from ..cli_steps import CliStepBase from .e2e_inputs import E2eInputs class SubmitVotesStep(CliStepBase): """Responsible for submitting ballots into a ballot store.""" def submit( self, e2e_inputs: E2eInputs, build_election_results: BuildElectionResults, e2e_encrypt_results: EncryptResults, ) -> DataStore: self.print_header("Submitting Ballots") internal_manifest = build_election_results.internal_manifest context = build_election_results.context ballot_store = SubmitVotesStep._cast_and_spoil( internal_manifest, context, e2e_encrypt_results.ciphertext_ballots, e2e_inputs, ) return ballot_store @staticmethod def _cast_and_spoil( internal_manifest: InternalManifest, context: CiphertextElectionContext, ciphertext_ballots: List[CiphertextBallot], e2e_inputs: E2eInputs, ) -> DataStore: ballot_store: DataStore = DataStore() ballot_box = BallotBox(internal_manifest, context, ballot_store) for ballot in ciphertext_ballots: spoil = ballot.object_id == e2e_inputs.spoil_id if spoil: submitted_ballot = ballot_box.spoil(ballot) else: submitted_ballot = ballot_box.cast(ballot) click.echo( f"Submitted Ballot Id: {ballot.object_id} state: {get_optional(submitted_ballot).state}" ) return ballot_store ================================================ FILE: src/electionguard_cli/encrypt_ballots/__init__.py ================================================ from electionguard_cli.encrypt_ballots import encrypt_ballot_inputs from electionguard_cli.encrypt_ballots import encrypt_ballots_election_builder_step from electionguard_cli.encrypt_ballots import encrypt_ballots_input_retrieval_step from electionguard_cli.encrypt_ballots import encrypt_ballots_publish_step from electionguard_cli.encrypt_ballots import encrypt_command from electionguard_cli.encrypt_ballots.encrypt_ballot_inputs import ( EncryptBallotInputs, ) from electionguard_cli.encrypt_ballots.encrypt_ballots_election_builder_step import ( EncryptBallotsElectionBuilderStep, ) from electionguard_cli.encrypt_ballots.encrypt_ballots_input_retrieval_step import ( EncryptBallotsInputRetrievalStep, ) from electionguard_cli.encrypt_ballots.encrypt_ballots_publish_step import ( EncryptBallotsPublishStep, ) from electionguard_cli.encrypt_ballots.encrypt_command import ( EncryptBallotsCommand, ) __all__ = [ "EncryptBallotInputs", "EncryptBallotsCommand", "EncryptBallotsElectionBuilderStep", "EncryptBallotsInputRetrievalStep", "EncryptBallotsPublishStep", "encrypt_ballot_inputs", "encrypt_ballots_election_builder_step", "encrypt_ballots_input_retrieval_step", "encrypt_ballots_publish_step", "encrypt_command", ] ================================================ FILE: src/electionguard_cli/encrypt_ballots/encrypt_ballot_inputs.py ================================================ from typing import List from electionguard.ballot import PlaintextBallot from electionguard.election import CiphertextElectionContext from electionguard.encrypt import EncryptionDevice from electionguard.manifest import Manifest from ..cli_models import ( CliElectionInputsBase, ) class EncryptBallotInputs(CliElectionInputsBase): """Responsible for holding the inputs for the CLI's encrypt ballots command""" def __init__( self, manifest: Manifest, context: CiphertextElectionContext, plaintext_ballots: List[PlaintextBallot], ): self.guardian_count = context.number_of_guardians self.quorum = context.quorum self.manifest = manifest self.plaintext_ballots = plaintext_ballots self.context = context plaintext_ballots: List[PlaintextBallot] context: CiphertextElectionContext encryption_devices: List[EncryptionDevice] output_record: str ================================================ FILE: src/electionguard_cli/encrypt_ballots/encrypt_ballots_election_builder_step.py ================================================ from ..cli_models import BuildElectionResults from ..cli_steps import ElectionBuilderStep from .encrypt_ballot_inputs import EncryptBallotInputs class EncryptBallotsElectionBuilderStep(ElectionBuilderStep): """Responsible for creating a manifest and context for use in an election specifically for the encrypt ballots command""" def build_election_with_context( self, election_inputs: EncryptBallotInputs ) -> BuildElectionResults: verification_url = election_inputs.context.get_extended_data_field( self.VERIFICATION_URL_NAME ) return self._build_election( election_inputs, election_inputs.context.elgamal_public_key, election_inputs.context.commitment_hash, verification_url, ) ================================================ FILE: src/electionguard_cli/encrypt_ballots/encrypt_ballots_input_retrieval_step.py ================================================ from io import TextIOWrapper from electionguard.ballot import PlaintextBallot from electionguard.manifest import Manifest from .encrypt_ballot_inputs import EncryptBallotInputs from ..cli_steps import ( InputRetrievalStepBase, ) class EncryptBallotsInputRetrievalStep(InputRetrievalStepBase): """Responsible for retrieving and parsing user provided inputs for the CLI's encrypt ballots command.""" def get_inputs( self, manifest_file: TextIOWrapper, context_file: TextIOWrapper, ballots_dir: str, ) -> EncryptBallotInputs: self.print_header("Retrieving Inputs") manifest: Manifest = self._get_manifest(manifest_file) context = InputRetrievalStepBase._get_context(context_file) plaintext_ballots = InputRetrievalStepBase._get_ballots( ballots_dir, PlaintextBallot ) return EncryptBallotInputs( manifest, context, plaintext_ballots, ) ================================================ FILE: src/electionguard_cli/encrypt_ballots/encrypt_ballots_publish_step.py ================================================ from click import echo from electionguard import to_file from ..cli_models import EncryptResults from ..cli_steps import OutputStepBase class EncryptBallotsPublishStep(OutputStepBase): """Responsible for writing the results of the encrypt ballots command.""" def publish(self, encrypt_results: EncryptResults, out_dir: str) -> None: if out_dir is None: return self.print_header("Writing Encrypted Ballots") device_file = to_file(encrypt_results.device, "device", out_dir) self.print_value("Device file", device_file + ".json") for ballot in encrypt_results.ciphertext_ballots: ballot_file = to_file(ballot, ballot.object_id, out_dir) echo(f"Writing {ballot_file}") self.print_value("Encrypted ballots", len(encrypt_results.ciphertext_ballots)) ================================================ FILE: src/electionguard_cli/encrypt_ballots/encrypt_command.py ================================================ from io import TextIOWrapper import click from .encrypt_ballots_election_builder_step import EncryptBallotsElectionBuilderStep from .encrypt_ballots_input_retrieval_step import EncryptBallotsInputRetrievalStep from .encrypt_ballots_publish_step import EncryptBallotsPublishStep from ..cli_steps import EncryptVotesStep @click.command("encrypt-ballots") @click.option( "--manifest", prompt="Manifest file", help="The location of an election manifest.", type=click.File(), ) @click.option( "--context", prompt="Context file", help="The location of an election context.", type=click.File(), ) @click.option( "--ballots-dir", prompt="Ballots file", help="The location of a file that contains plaintext ballots.", type=click.Path(exists=True, dir_okay=True, file_okay=False, resolve_path=True), ) @click.option( "--out-dir", help="A directory for saving encrypted ballots and encryption device to.", type=click.Path(exists=False, dir_okay=True, file_okay=False, resolve_path=True), ) def EncryptBallotsCommand( manifest: TextIOWrapper, context: TextIOWrapper, ballots_dir: str, out_dir: str ) -> None: """ Encrypt ballots, but does not submit them """ election_inputs = EncryptBallotsInputRetrievalStep().get_inputs( manifest, context, ballots_dir ) build_election_results = ( EncryptBallotsElectionBuilderStep().build_election_with_context(election_inputs) ) encrypt_results = EncryptVotesStep().encrypt( election_inputs.plaintext_ballots, build_election_results ) EncryptBallotsPublishStep().publish(encrypt_results, out_dir) ================================================ FILE: src/electionguard_cli/import_ballots/__init__.py ================================================ from electionguard_cli.import_ballots import import_ballot_inputs from electionguard_cli.import_ballots import import_ballots_command from electionguard_cli.import_ballots import import_ballots_election_builder_step from electionguard_cli.import_ballots import import_ballots_input_retrieval_step from electionguard_cli.import_ballots import import_ballots_publish_step from electionguard_cli.import_ballots.import_ballot_inputs import ( ImportBallotInputs, ) from electionguard_cli.import_ballots.import_ballots_command import ( ImportBallotsCommand, ) from electionguard_cli.import_ballots.import_ballots_election_builder_step import ( ImportBallotsElectionBuilderStep, ) from electionguard_cli.import_ballots.import_ballots_input_retrieval_step import ( ImportBallotsInputRetrievalStep, ) from electionguard_cli.import_ballots.import_ballots_publish_step import ( ImportBallotsPublishStep, ) __all__ = [ "ImportBallotInputs", "ImportBallotsCommand", "ImportBallotsElectionBuilderStep", "ImportBallotsInputRetrievalStep", "ImportBallotsPublishStep", "import_ballot_inputs", "import_ballots_command", "import_ballots_election_builder_step", "import_ballots_input_retrieval_step", "import_ballots_publish_step", ] ================================================ FILE: src/electionguard_cli/import_ballots/import_ballot_inputs.py ================================================ from typing import List from electionguard.ballot import SubmittedBallot from electionguard.election import CiphertextElectionContext from electionguard.encrypt import EncryptionDevice from electionguard.guardian import Guardian from electionguard.manifest import Manifest from ..cli_models import ( CliElectionInputsBase, ) # pylint: disable=too-many-instance-attributes class ImportBallotInputs(CliElectionInputsBase): """Responsible for holding the inputs for the CLI's import ballots command""" def __init__( self, guardians: List[Guardian], manifest: Manifest, submitted_ballots: List[SubmittedBallot], context: CiphertextElectionContext, encryption_device: List[EncryptionDevice], output_record: str, ): self.guardian_count = context.number_of_guardians self.quorum = context.quorum self.guardians = guardians self.manifest = manifest self.submitted_ballots = submitted_ballots self.context = context self.encryption_devices = encryption_device self.output_record = output_record submitted_ballots: List[SubmittedBallot] context: CiphertextElectionContext encryption_devices: List[EncryptionDevice] output_record: str ================================================ FILE: src/electionguard_cli/import_ballots/import_ballots_command.py ================================================ from io import TextIOWrapper import click from .import_ballots_publish_step import ImportBallotsPublishStep from .import_ballots_input_retrieval_step import ImportBallotsInputRetrievalStep from .import_ballots_election_builder_step import ImportBallotsElectionBuilderStep from ..cli_steps.decrypt_step import DecryptStep from ..cli_steps.print_results_step import PrintResultsStep from ..cli_steps.tally_step import TallyStep @click.command("import-ballots") @click.option( "--manifest", prompt="Manifest file", help="The location of an election manifest.", type=click.File(), ) @click.option( "--context", prompt="Context file", help="The location of an election context.", type=click.File(), ) @click.option( "--ballots-dir", prompt="Ballots file", help="The location of a file that contains submitted ballots.", type=click.Path(exists=True, dir_okay=True, file_okay=False, resolve_path=True), ) @click.option( "--guardian-keys", prompt="Guardian keys file", help="The location of a json file with all guardians's private key data. " + "This corresponds to the output-keys parameter of the e2e command.", type=click.Path(exists=True, dir_okay=True, file_okay=False, resolve_path=True), ) @click.option( "--encryption-device", prompt="Encryption device file", help="An optional file containing an encryption device used for the ballots. This data will " + "be exported in the election record.", type=click.Path(exists=True, dir_okay=False, file_okay=True, resolve_path=True), default=None, ) @click.option( "--output-record", help="A file name for saving an output election record (e.g. './election.zip')." + " If no value provided then an election record will not be generated.", type=click.Path( exists=False, dir_okay=False, file_okay=True, ), default=None, ) def ImportBallotsCommand( manifest: TextIOWrapper, context: TextIOWrapper, ballots_dir: str, guardian_keys: str, encryption_device: str, output_record: str, ) -> None: """ Imports ballots """ # get user inputs election_inputs = ImportBallotsInputRetrievalStep().get_inputs( manifest, context, ballots_dir, guardian_keys, encryption_device, output_record ) # perform election build_election_results = ( ImportBallotsElectionBuilderStep().build_election_with_context(election_inputs) ) (ciphertext_tally, spoiled_ballots) = TallyStep().get_from_ballots( build_election_results, election_inputs.submitted_ballots ) decrypt_results = DecryptStep().decrypt( ciphertext_tally, spoiled_ballots, election_inputs.guardians, build_election_results, election_inputs.manifest, ) # print results PrintResultsStep().print_election_results(decrypt_results, election_inputs.manifest) # publish election record ImportBallotsPublishStep().publish( election_inputs, build_election_results, decrypt_results ) ================================================ FILE: src/electionguard_cli/import_ballots/import_ballots_election_builder_step.py ================================================ from ..cli_models import BuildElectionResults from ..cli_steps import ElectionBuilderStep from .import_ballot_inputs import ImportBallotInputs class ImportBallotsElectionBuilderStep(ElectionBuilderStep): """Responsible for creating a manifest and context for use in an election specifically for the import ballots command""" def build_election_with_context( self, election_inputs: ImportBallotInputs ) -> BuildElectionResults: verification_url = election_inputs.context.get_extended_data_field( self.VERIFICATION_URL_NAME ) return self._build_election( election_inputs, election_inputs.context.elgamal_public_key, election_inputs.context.commitment_hash, verification_url, ) ================================================ FILE: src/electionguard_cli/import_ballots/import_ballots_input_retrieval_step.py ================================================ from typing import List from io import TextIOWrapper from os import listdir from os.path import join from electionguard import CiphertextElectionContext from electionguard.ballot import SubmittedBallot from electionguard.encrypt import EncryptionDevice from electionguard.guardian import Guardian, PrivateGuardianRecord from electionguard.manifest import Manifest from electionguard.serialize import from_file from .import_ballot_inputs import ( ImportBallotInputs, ) from ..cli_steps import ( InputRetrievalStepBase, ) class ImportBallotsInputRetrievalStep(InputRetrievalStepBase): """Responsible for retrieving and parsing user provided inputs for the CLI's import ballots command.""" def get_inputs( self, manifest_file: TextIOWrapper, context_file: TextIOWrapper, ballots_dir: str, guardian_keys: str, encryption_device_file: str, output_record: str, ) -> ImportBallotInputs: self.print_header("Retrieving Inputs") manifest: Manifest = self._get_manifest(manifest_file) context = InputRetrievalStepBase._get_context(context_file) guardians = ImportBallotsInputRetrievalStep._get_guardians_from_keys( guardian_keys, context ) encryption_devices = self._get_encryption_devices(encryption_device_file) submitted_ballots = ImportBallotsInputRetrievalStep._get_ballots( ballots_dir, SubmittedBallot ) self.print_value("Ballots Dir", ballots_dir) return ImportBallotInputs( guardians, manifest, submitted_ballots, context, encryption_devices, output_record, ) def _get_encryption_devices( self, encryption_device_file: str ) -> List[EncryptionDevice]: if encryption_device_file is None: return [] encryption_device = from_file(EncryptionDevice, encryption_device_file) self.print_value("Encryption device id", encryption_device.device_id) self.print_value("Encryption device location", encryption_device.location) return [encryption_device] @staticmethod def _get_guardians_from_keys( guardian_keys_dir: str, context: CiphertextElectionContext ) -> List[Guardian]: files = listdir(guardian_keys_dir) private_records = [ ImportBallotsInputRetrievalStep._load_private_record(guardian_keys_dir, f) for f in files ] return list( map( lambda record: ImportBallotsInputRetrievalStep._get_guardian( record, context ), private_records, ) ) @staticmethod def _load_private_record(guardian_dir: str, filename: str) -> PrivateGuardianRecord: full_file = join(guardian_dir, filename) return from_file(PrivateGuardianRecord, full_file) @staticmethod def _get_guardian( private_record: PrivateGuardianRecord, context: CiphertextElectionContext ) -> Guardian: return Guardian.from_private_record( private_record, context.number_of_guardians, context.quorum ) ================================================ FILE: src/electionguard_cli/import_ballots/import_ballots_publish_step.py ================================================ from typing import List from shutil import make_archive from os.path import splitext from tempfile import TemporaryDirectory from click import echo from electionguard.encrypt import EncryptionDevice from electionguard.constants import get_constants from electionguard_tools.helpers.export import export_record from .import_ballot_inputs import ImportBallotInputs from ..cli_models import CliDecryptResults, BuildElectionResults from ..cli_steps import OutputStepBase class ImportBallotsPublishStep(OutputStepBase): """Responsible for publishing an election record during an import ballots command""" def publish( self, election_inputs: ImportBallotInputs, build_election_results: BuildElectionResults, decrypt_results: CliDecryptResults, ) -> None: if election_inputs.output_record is None: return self.print_header("Publishing Results") guardian_records = OutputStepBase._get_guardian_records(election_inputs) constants = get_constants() encryption_devices: List[EncryptionDevice] = election_inputs.encryption_devices with TemporaryDirectory() as temp_dir: export_record( election_inputs.manifest, build_election_results.context, constants, encryption_devices, election_inputs.submitted_ballots, decrypt_results.plaintext_spoiled_ballots.values(), decrypt_results.ciphertext_tally.publish(), decrypt_results.plaintext_tally, guardian_records, decrypt_results.lagrange_coefficients, election_record_directory=temp_dir, ) file_name = splitext(election_inputs.output_record)[0] make_archive(file_name, self._COMPRESSION_FORMAT, temp_dir) echo(f"Exported election record to '{election_inputs.output_record}'") ================================================ FILE: src/electionguard_cli/mark_ballots/__init__.py ================================================ from electionguard_cli.mark_ballots import mark_ballot_inputs from electionguard_cli.mark_ballots import mark_ballots_election_builder_step from electionguard_cli.mark_ballots import mark_ballots_input_retrieval_step from electionguard_cli.mark_ballots import mark_ballots_publish_step from electionguard_cli.mark_ballots import mark_command from electionguard_cli.mark_ballots.mark_ballot_inputs import ( MarkBallotInputs, ) from electionguard_cli.mark_ballots.mark_ballots_election_builder_step import ( MarkBallotsElectionBuilderStep, ) from electionguard_cli.mark_ballots.mark_ballots_input_retrieval_step import ( MarkBallotsInputRetrievalStep, ) from electionguard_cli.mark_ballots.mark_ballots_publish_step import ( MarkBallotsPublishStep, ) from electionguard_cli.mark_ballots.mark_command import ( MarkBallotsCommand, ) __all__ = [ "MarkBallotInputs", "MarkBallotsCommand", "MarkBallotsElectionBuilderStep", "MarkBallotsInputRetrievalStep", "MarkBallotsPublishStep", "mark_ballot_inputs", "mark_ballots_election_builder_step", "mark_ballots_input_retrieval_step", "mark_ballots_publish_step", "mark_command", ] ================================================ FILE: src/electionguard_cli/mark_ballots/mark_ballot_inputs.py ================================================ from electionguard.election import CiphertextElectionContext from electionguard.manifest import Manifest from ..cli_models import ( CliElectionInputsBase, ) class MarkBallotInputs(CliElectionInputsBase): """Responsible for holding the inputs for the CLI's mark ballots command""" def __init__( self, manifest: Manifest, context: CiphertextElectionContext, ): self.guardian_count = context.number_of_guardians self.quorum = context.quorum self.manifest = manifest self.context = context context: CiphertextElectionContext ================================================ FILE: src/electionguard_cli/mark_ballots/mark_ballots_election_builder_step.py ================================================ from ..cli_models import BuildElectionResults from ..cli_steps import ElectionBuilderStep from .mark_ballot_inputs import MarkBallotInputs class MarkBallotsElectionBuilderStep(ElectionBuilderStep): """Responsible for creating a manifest and context for use in an election specifically for the mark ballots command""" def build_election_with_context( self, election_inputs: MarkBallotInputs ) -> BuildElectionResults: verification_url = election_inputs.context.get_extended_data_field( self.VERIFICATION_URL_NAME ) return self._build_election( election_inputs, election_inputs.context.elgamal_public_key, election_inputs.context.commitment_hash, verification_url, ) ================================================ FILE: src/electionguard_cli/mark_ballots/mark_ballots_input_retrieval_step.py ================================================ from io import TextIOWrapper from electionguard.manifest import Manifest from .mark_ballot_inputs import MarkBallotInputs from ..cli_steps import ( InputRetrievalStepBase, ) class MarkBallotsInputRetrievalStep(InputRetrievalStepBase): """Responsible for retrieving and parsing user provided inputs for the CLI's mark ballots command.""" def get_inputs( self, manifest_file: TextIOWrapper, context_file: TextIOWrapper, ) -> MarkBallotInputs: self.print_header("Retrieving Inputs") manifest: Manifest = self._get_manifest(manifest_file) context = InputRetrievalStepBase._get_context(context_file) return MarkBallotInputs( manifest, context, ) ================================================ FILE: src/electionguard_cli/mark_ballots/mark_ballots_publish_step.py ================================================ from click import echo from electionguard import to_file from ..cli_models import MarkResults from ..cli_steps import OutputStepBase class MarkBallotsPublishStep(OutputStepBase): """Responsible for writing the results of the mark ballots command.""" def publish(self, marked_ballots: MarkResults, out_dir: str) -> None: if out_dir is None: return self.print_header("Writing Marked Ballots") for ballot in marked_ballots.plaintext_ballots: ballot_file = to_file(ballot, ballot.object_id, out_dir) echo(f"Writing {ballot_file}") self.print_value("Marked ballots", len(marked_ballots.plaintext_ballots)) ================================================ FILE: src/electionguard_cli/mark_ballots/mark_command.py ================================================ from io import TextIOWrapper import click from .mark_ballots_election_builder_step import MarkBallotsElectionBuilderStep from .mark_ballots_input_retrieval_step import MarkBallotsInputRetrievalStep from .mark_ballots_publish_step import MarkBallotsPublishStep from ..cli_steps import MarkBallotsStep @click.command("mark-ballots") @click.argument("num_ballots", type=click.INT) @click.argument("ballot_style_id", type=click.STRING, required=False) @click.option( "--manifest", prompt="Manifest file", help="The location of an election manifest.", type=click.File(), ) @click.option( "--context", prompt="Context file", help="The location of an election context.", type=click.File(), ) @click.option( "--out-dir", help="A directory for saving plaintext ballots to.", type=click.Path(exists=False, dir_okay=True, file_okay=False, resolve_path=True), ) def MarkBallotsCommand( num_ballots: int, ballot_style_id: str, manifest: TextIOWrapper, context: TextIOWrapper, out_dir: str, ) -> None: """ Marks ballots """ election_inputs = MarkBallotsInputRetrievalStep().get_inputs(manifest, context) build_election_results = ( MarkBallotsElectionBuilderStep().build_election_with_context(election_inputs) ) marked_ballots = MarkBallotsStep().mark( build_election_results, num_ballots, ballot_style_id ) MarkBallotsPublishStep().publish(marked_ballots, out_dir) ================================================ FILE: src/electionguard_cli/setup_election/__init__.py ================================================ from electionguard_cli.setup_election import output_setup_files_step from electionguard_cli.setup_election import setup_election_builder_step from electionguard_cli.setup_election import setup_election_command from electionguard_cli.setup_election import setup_input_retrieval_step from electionguard_cli.setup_election import setup_inputs from electionguard_cli.setup_election.output_setup_files_step import ( OutputSetupFilesStep, ) from electionguard_cli.setup_election.setup_election_builder_step import ( SetupElectionBuilderStep, ) from electionguard_cli.setup_election.setup_election_command import ( SetupElectionCommand, ) from electionguard_cli.setup_election.setup_input_retrieval_step import ( SetupInputRetrievalStep, ) from electionguard_cli.setup_election.setup_inputs import ( SetupInputs, ) __all__ = [ "OutputSetupFilesStep", "SetupElectionBuilderStep", "SetupElectionCommand", "SetupInputRetrievalStep", "SetupInputs", "output_setup_files_step", "setup_election_builder_step", "setup_election_command", "setup_input_retrieval_step", "setup_inputs", ] ================================================ FILE: src/electionguard_cli/setup_election/output_setup_files_step.py ================================================ from os import path from os.path import join from typing import Optional import click from electionguard.election import CiphertextElectionContext from electionguard.serialize import to_file from electionguard.constants import get_constants from electionguard_tools.helpers.export import ( CONSTANTS_FILE_NAME, CONTEXT_FILE_NAME, GUARDIAN_PREFIX, MANIFEST_FILE_NAME, ) from .setup_inputs import SetupInputs from ..cli_models.e2e_build_election_results import BuildElectionResults from ..cli_steps import OutputStepBase class OutputSetupFilesStep(OutputStepBase): """Responsible for outputting the files necessary to setup an election""" def output( self, setup_inputs: SetupInputs, build_election_results: BuildElectionResults, package_dir: str, keys_dir: Optional[str], ) -> None: self.print_header("Generating Output") self._export_context(build_election_results.context, package_dir) self._export_constants(package_dir) self._export_manifest(setup_inputs, package_dir) self._export_guardian_records(setup_inputs, package_dir) if keys_dir is not None: self._export_guardian_private_keys(setup_inputs, keys_dir) def _export_context( self, context: CiphertextElectionContext, out_dir: str, ) -> str: return self._export_file("Context", context, out_dir, CONTEXT_FILE_NAME) def _export_constants(self, out_dir: str) -> str: constants = get_constants() return self._export_file("Constants", constants, out_dir, CONSTANTS_FILE_NAME) def _export_manifest(self, setup_inputs: SetupInputs, out_dir: str) -> None: self._export_file( "Manifest", setup_inputs.manifest, out_dir, MANIFEST_FILE_NAME, ) def _export_guardian_records(self, setup_inputs: SetupInputs, out_dir: str) -> None: guardian_records_dir = join(out_dir, "guardians") guardian_records = OutputStepBase._get_guardian_records(setup_inputs) for guardian_record in guardian_records: to_file( guardian_record, GUARDIAN_PREFIX + guardian_record.guardian_id, guardian_records_dir, ) self.print_value("Guardian records", guardian_records_dir) def _export_guardian_private_keys(self, inputs: SetupInputs, keys_dir: str) -> None: if path.exists(keys_dir) and not inputs.force: confirm = click.confirm( "Existing guardian keys found, are you sure you want to overwrite them?", default=True, ) if not confirm: return self._export_private_keys(keys_dir, inputs.guardians) ================================================ FILE: src/electionguard_cli/setup_election/setup_election_builder_step.py ================================================ from electionguard import ElectionJointKey from .setup_inputs import SetupInputs from ..cli_models import BuildElectionResults from ..cli_steps import ElectionBuilderStep class SetupElectionBuilderStep(ElectionBuilderStep): """Responsible for creating a manifest and context for use in an election specifically for the import ballots command""" def build_election_for_setup( self, election_inputs: SetupInputs, joint_key: ElectionJointKey ) -> BuildElectionResults: return self._build_election( election_inputs, joint_key.joint_public_key, joint_key.commitment_hash, election_inputs.verification_url, ) ================================================ FILE: src/electionguard_cli/setup_election/setup_election_command.py ================================================ from io import TextIOWrapper import click from .setup_election_builder_step import SetupElectionBuilderStep from .output_setup_files_step import OutputSetupFilesStep from ..cli_steps import KeyCeremonyStep from .setup_input_retrieval_step import SetupInputRetrievalStep @click.command("setup") @click.option( "--guardian-count", prompt="Number of guardians", help="The number of guardians that will participate in the key ceremony and tally.", type=click.INT, ) @click.option( "--quorum", prompt="Quorum", help="The minimum number of guardians required to show up to the tally.", type=click.INT, ) @click.option( "--manifest", prompt="Manifest file", help="The location of an election manifest.", type=click.File(), ) @click.option( "--url", help="An optional verification url for the election.", required=False, type=click.STRING, default=None, prompt=False, ) @click.option( "--package-dir", prompt="Election Package Output Directory", help="The location of a directory into which will be placed the output files such as " + "context, constants, and guardian keys. Existing files will be overwritten.", type=click.Path(exists=False, dir_okay=True, file_okay=False, resolve_path=True), ) @click.option( "--keys-dir", prompt="Private guardian keys directory", help="The location of a directory into which will be placed the guardian's private keys " + "This folder should be protected. Existing files will be overwritten.", type=click.Path(exists=False, dir_okay=True, file_okay=False, resolve_path=True), ) def SetupElectionCommand( guardian_count: int, quorum: int, manifest: TextIOWrapper, url: str, package_dir: str, keys_dir: str, ) -> None: """ This command runs an automated key ceremony and produces the files necessary to encrypt ballots, decrypt an election, and produce an election record. """ setup_inputs = SetupInputRetrievalStep().get_inputs( guardian_count, quorum, manifest, url ) joint_key = KeyCeremonyStep().run_key_ceremony(setup_inputs.guardians) build_election_results = SetupElectionBuilderStep().build_election_for_setup( setup_inputs, joint_key ) OutputSetupFilesStep().output( setup_inputs, build_election_results, package_dir, keys_dir ) ================================================ FILE: src/electionguard_cli/setup_election/setup_input_retrieval_step.py ================================================ from io import TextIOWrapper from typing import Optional from electionguard.key_ceremony import CeremonyDetails from electionguard.manifest import Manifest from electionguard_tools.helpers.key_ceremony_orchestrator import ( KeyCeremonyOrchestrator, ) from .setup_inputs import SetupInputs from ..cli_steps import InputRetrievalStepBase class SetupInputRetrievalStep(InputRetrievalStepBase): """Responsible for retrieving and parsing user provided inputs for the CLI's setup election command.""" def get_inputs( self, guardian_count: int, quorum: int, manifest_file: TextIOWrapper, verification_url: Optional[str], ) -> SetupInputs: self.print_header("Retrieving Inputs") guardians = KeyCeremonyOrchestrator.create_guardians( CeremonyDetails(guardian_count, quorum) ) manifest: Manifest = self._get_manifest(manifest_file) return SetupInputs( guardian_count, quorum, guardians, manifest, verification_url ) ================================================ FILE: src/electionguard_cli/setup_election/setup_inputs.py ================================================ from typing import List, Optional from electionguard.guardian import Guardian from electionguard.manifest import Manifest from ..cli_models import CliElectionInputsBase class SetupInputs(CliElectionInputsBase): """Responsible for holding the inputs for the CLI's setup election command.""" verification_url: Optional[str] force: bool def __init__( self, guardian_count: int, quorum: int, guardians: List[Guardian], manifest: Manifest, verification_url: Optional[str], force: bool = False, ): self.guardian_count = guardian_count self.quorum = quorum self.guardians = guardians self.manifest = manifest self.verification_url = verification_url self.force = force ================================================ FILE: src/electionguard_cli/start.py ================================================ import click from .setup_election.setup_election_command import SetupElectionCommand from .e2e.e2e_command import E2eCommand from .import_ballots.import_ballots_command import ImportBallotsCommand from .encrypt_ballots.encrypt_command import EncryptBallotsCommand from .mark_ballots.mark_command import MarkBallotsCommand from .submit_ballots.submit_command import SubmitBallotsCommand @click.group() def cli() -> None: pass cli.add_command(E2eCommand) cli.add_command(SetupElectionCommand) cli.add_command(MarkBallotsCommand) cli.add_command(EncryptBallotsCommand) cli.add_command(SubmitBallotsCommand) cli.add_command(ImportBallotsCommand) ================================================ FILE: src/electionguard_cli/submit_ballots/__init__.py ================================================ from electionguard_cli.submit_ballots import submit_ballot_inputs from electionguard_cli.submit_ballots import submit_ballots_election_builder_step from electionguard_cli.submit_ballots import submit_ballots_input_retrieval_step from electionguard_cli.submit_ballots import submit_ballots_publish_step from electionguard_cli.submit_ballots import submit_command from electionguard_cli.submit_ballots.submit_ballot_inputs import ( SubmitBallotInputs, ) from electionguard_cli.submit_ballots.submit_ballots_election_builder_step import ( SubmitBallotsElectionBuilderStep, ) from electionguard_cli.submit_ballots.submit_ballots_input_retrieval_step import ( SubmitBallotsInputRetrievalStep, ) from electionguard_cli.submit_ballots.submit_ballots_publish_step import ( SubmitBallotsPublishStep, ) from electionguard_cli.submit_ballots.submit_command import ( SubmitBallotsCommand, ) __all__ = [ "SubmitBallotInputs", "SubmitBallotsCommand", "SubmitBallotsElectionBuilderStep", "SubmitBallotsInputRetrievalStep", "SubmitBallotsPublishStep", "submit_ballot_inputs", "submit_ballots_election_builder_step", "submit_ballots_input_retrieval_step", "submit_ballots_publish_step", "submit_command", ] ================================================ FILE: src/electionguard_cli/submit_ballots/submit_ballot_inputs.py ================================================ from typing import List from electionguard.election import CiphertextElectionContext from electionguard.manifest import Manifest from electionguard.ballot import CiphertextBallot from ..cli_models import ( CliElectionInputsBase, ) class SubmitBallotInputs(CliElectionInputsBase): """Responsible for holding the inputs for the CLI's submit ballots command""" def __init__( self, manifest: Manifest, context: CiphertextElectionContext, cast_ballots: List[CiphertextBallot], spoil_ballots: List[CiphertextBallot], ): self.guardian_count = context.number_of_guardians self.quorum = context.quorum self.manifest = manifest self.context = context self.cast_ballots = cast_ballots self.spoil_ballots = spoil_ballots context: CiphertextElectionContext ================================================ FILE: src/electionguard_cli/submit_ballots/submit_ballots_election_builder_step.py ================================================ from ..cli_models import BuildElectionResults from ..cli_steps import ElectionBuilderStep from .submit_ballot_inputs import SubmitBallotInputs class SubmitBallotsElectionBuilderStep(ElectionBuilderStep): """Responsible for creating a manifest and context for use in an election specifically for the submit ballots command""" def build_election_with_context( self, election_inputs: SubmitBallotInputs ) -> BuildElectionResults: verification_url = election_inputs.context.get_extended_data_field( self.VERIFICATION_URL_NAME ) return self._build_election( election_inputs, election_inputs.context.elgamal_public_key, election_inputs.context.commitment_hash, verification_url, ) ================================================ FILE: src/electionguard_cli/submit_ballots/submit_ballots_input_retrieval_step.py ================================================ from io import TextIOWrapper from electionguard.manifest import Manifest from electionguard.ballot import CiphertextBallot from .submit_ballot_inputs import SubmitBallotInputs from ..cli_steps import ( InputRetrievalStepBase, ) class SubmitBallotsInputRetrievalStep(InputRetrievalStepBase): """Responsible for retrieving and parsing user provided inputs for the CLI's submit ballots command.""" def get_inputs( self, manifest_file: TextIOWrapper, context_file: TextIOWrapper, cast_ballots_dir: str, spoil_ballots_dir: str, ) -> SubmitBallotInputs: self.print_header("Retrieving Inputs") manifest: Manifest = self._get_manifest(manifest_file) context = InputRetrievalStepBase._get_context(context_file) cast_ballots = self._get_ballots(cast_ballots_dir, CiphertextBallot) if spoil_ballots_dir is not None: spoil_ballots = self._get_ballots(spoil_ballots_dir, CiphertextBallot) else: spoil_ballots = [] return SubmitBallotInputs( manifest, context, cast_ballots, spoil_ballots, ) ================================================ FILE: src/electionguard_cli/submit_ballots/submit_ballots_publish_step.py ================================================ from click import echo from electionguard import to_file from ..cli_models import SubmitResults from ..cli_steps import OutputStepBase class SubmitBallotsPublishStep(OutputStepBase): """Responsible for writing the results of the submit ballots command.""" def publish(self, submit_results: SubmitResults, out_dir: str) -> None: if out_dir is None: return self.print_header("Writing Submitted Ballots") for ballot in submit_results.submitted_ballots: ballot_file = to_file(ballot, ballot.object_id, out_dir) echo(f"Writing {ballot_file}") self.print_value("Submitted ballots", len(submit_results.submitted_ballots)) ================================================ FILE: src/electionguard_cli/submit_ballots/submit_command.py ================================================ from io import TextIOWrapper import click from .submit_ballots_election_builder_step import SubmitBallotsElectionBuilderStep from .submit_ballots_input_retrieval_step import SubmitBallotsInputRetrievalStep from .submit_ballots_publish_step import SubmitBallotsPublishStep from ..cli_steps import SubmitBallotsStep @click.command("submit-ballots") @click.argument( "cast_ballots_dir", type=click.Path(exists=False, dir_okay=True, file_okay=False, resolve_path=True), ) @click.argument( "spoil_ballots_dir", type=click.Path(exists=False, dir_okay=True, file_okay=False, resolve_path=True), required=False, ) @click.option( "--manifest", prompt="Manifest file", help="The location of an election manifest.", type=click.File(), ) @click.option( "--context", prompt="Context file", help="The location of an election context.", type=click.File(), ) @click.option( "--out-dir", help="A directory for saving plaintext ballots to.", type=click.Path(exists=False, dir_okay=True, file_okay=False, resolve_path=True), ) def SubmitBallotsCommand( cast_ballots_dir: str, spoil_ballots_dir: str, manifest: TextIOWrapper, context: TextIOWrapper, out_dir: str, ) -> None: """ Submits ballots """ election_inputs = SubmitBallotsInputRetrievalStep().get_inputs( manifest, context, cast_ballots_dir, spoil_ballots_dir ) build_election_results = ( SubmitBallotsElectionBuilderStep().build_election_with_context(election_inputs) ) submitted_ballots = SubmitBallotsStep().submit( build_election_results, election_inputs.cast_ballots, election_inputs.spoil_ballots, ) SubmitBallotsPublishStep().publish(submitted_ballots, out_dir) ================================================ FILE: src/electionguard_db/docker-compose.db.yml ================================================ version: "3.8" services: mongo: image: mongo:4.4 container_name: "electionguard-db" restart: always ports: - 27017:27017 environment: MONGO_INITDB_ROOT_USERNAME: root MONGO_INITDB_ROOT_PASSWORD: ${EG_DB_PASSWORD} MONGO_INITDB_DATABASE: ElectionGuardDb volumes: - ./mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js:ro - ${EG_DB_DIR}:/data/db mongo-express: image: mongo-express:1.0.0-alpha.4 restart: always ports: - 8181:8081 environment: ME_CONFIG_MONGODB_SERVER: electionguard-db ME_CONFIG_MONGODB_ADMINUSERNAME: root ME_CONFIG_MONGODB_ADMINPASSWORD: ${EG_DB_PASSWORD} depends_on: - mongo volumes: - ${EG_DB_DIR}:/data/db ================================================ FILE: src/electionguard_db/mongo-init.js ================================================ db.createCollection("guardians"); db.createCollection("key_ceremonies"); db.createCollection("elections"); db.createCollection("ballot_uploads"); db.createCollection("decryptions"); db.createCollection("db_deltas", { capped: true, size: 100000 }); db.db_deltas.insert({ type: "init" }); db.ballot_uploads.createIndex({ election_id: 1 }); db.ballot_uploads.createIndex({ election_id: 1, object_id: 1 }); db.decryptions.createIndex({ decryption_name: 1 }); db.decryptions.createIndex({ election_id: 1 }); db.decryptions.createIndex({ completed_at: 1 }); db.key_ceremonies.createIndex({ completed_at: 1 }); db.key_ceremonies.createIndex({ key_ceremony_name: 1 }); ================================================ FILE: src/electionguard_gui/.dockerignore ================================================ docker-compose*.yml Dockerfile ================================================ FILE: src/electionguard_gui/Dockerfile ================================================ FROM ubuntu:22.04 ################################################################################## # Install pyenv (https://github.com/pyenv/pyenv/wiki#suggested-build-environment) ################################################################################## RUN echo "installing pyenv" RUN apt-get update # install tzdata to remove "Please select the geographic area in which you live" when installing pyenv dependencies RUN apt-get install -y tzdata && \ apt-get install -y make build-essential libssl-dev zlib1g-dev \ libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm \ libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev RUN apt-get install -y git apt-utils # install pyenv RUN curl https://pyenv.run | bash # add pyenv to path RUN echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bashrc && \ echo 'command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bashrc && \ echo 'eval "$(pyenv init -)"' >> ~/.bashrc && \ echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.profile && \ echo 'command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.profile && \ echo 'eval "$(pyenv init -)"' >> ~/.profile ENV PYENV_ROOT="/root/.pyenv" ENV PATH="$PYENV_ROOT/bin:$PYENV_ROOT/shims:${PATH}" RUN echo $PATH ################################################################################## # Install python ################################################################################## RUN pyenv install 3.9.9 && \ pyenv global 3.9.9 ################################################################################## # Install EG prerequisites ################################################################################## RUN apt-get install -y libgmp-dev libmpfr-dev libmpc-dev RUN pip install 'poetry==2.2.1' ################################################################################## # Poetry Install ################################################################################## RUN mkdir /app WORKDIR /app # --no-root allows us to copy the minimum to just get dependencies to give # Docker very few reasons to invalidate the poetry install layer COPY pyproject.toml README.md ./ RUN poetry config virtualenvs.in-project true && \ poetry install --no-root ################################################################################## # Get Source ################################################################################## # cleanup first, the next layer will get invalidated easiliy RUN apt-get clean && \ rm -rf /var/lib/apt/lists/* COPY ./src ./src RUN rm src/electionguard_gui/__init__.py ################################################################################## # Run EGUI ################################################################################## # the final poetry install runs fast, it activates the virtualenv and initializes the modules RUN poetry install ENTRYPOINT ["poetry", "run", "egui"] # alternately, for testing: # ENTRYPOINT ["tail", "-f", "/dev/null"] ================================================ FILE: src/electionguard_gui/__init__.py ================================================ from electionguard_gui import components from electionguard_gui import containers from electionguard_gui import eel_utils from electionguard_gui import main_app from electionguard_gui import models from electionguard_gui import services from electionguard_gui import start from electionguard_gui.components import ( ComponentBase, CreateDecryptionComponent, CreateElectionComponent, CreateKeyCeremonyComponent, ElectionListComponent, ExportElectionRecordComponent, ExportEncryptionPackageComponent, GuardianHomeComponent, KeyCeremonyDetailsComponent, UploadBallotsComponent, ViewDecryptionComponent, ViewElectionComponent, ViewSpoiledBallotComponent, ViewTallyComponent, component_base, create_decryption_component, create_election_component, create_key_ceremony_component, election_list_component, export_election_record_component, export_encryption_package_component, get_spoiled_ballot_by_id, guardian_home_component, key_ceremony_details_component, notify_ui_db_changed, refresh_decryption, update_upload_status, upload_ballots_component, view_decryption_component, view_election_component, view_spoiled_ballot_component, view_tally_component, ) from electionguard_gui.containers import ( Container, ) from electionguard_gui.eel_utils import ( convert_utc_to_local, eel_fail, eel_success, utc_to_str, ) from electionguard_gui.main_app import ( MainApp, ) from electionguard_gui.models import ( DecryptionDto, ElectionDto, GuardianDecryptionShare, KeyCeremonyDto, KeyCeremonyStates, decryption_dto, election_dto, key_ceremony_dto, key_ceremony_states, ) from electionguard_gui.services import ( AuthorizationService, BallotUploadService, ConfigurationService, DB_HOST_KEY, DB_PASSWORD_KEY, DOCKER_MOUNT_DIR, DbService, DbWatcherService, DecryptionS1JoinService, DecryptionS2AnnounceService, DecryptionService, DecryptionStageBase, EelLogService, ElectionService, GuardianService, GuiSetupInputRetrievalStep, HOST_KEY, IS_ADMIN_KEY, KeyCeremonyS1JoinService, KeyCeremonyS2AnnounceService, KeyCeremonyS3MakeBackupService, KeyCeremonyS4ShareBackupService, KeyCeremonyS5VerifyBackupService, KeyCeremonyS6PublishKeyService, KeyCeremonyService, KeyCeremonyStageBase, KeyCeremonyStateService, MODE_KEY, PORT_KEY, RetryException, ServiceBase, VersionService, announce_guardians, authorization_service, backup_to_dict, ballot_upload_service, configuration_service, db_serialization_service, db_service, db_watcher_service, decryption_s1_join_service, decryption_s2_announce_service, decryption_service, decryption_stage_base, decryption_stages, directory_service, eel_log_service, election_service, export_service, get_data_dir, get_export_dir, get_export_locations, get_guardian_number, get_key_ceremony_status, get_plaintext_ballot_report, get_removable_drives, get_tally, guardian_service, gui_setup_input_retrieval_step, joint_key_to_dict, key_ceremony_s1_join_service, key_ceremony_s2_announce_service, key_ceremony_s3_make_backup_service, key_ceremony_s4_share_backup_service, key_ceremony_s5_verify_backup_service, key_ceremony_s6_publish_key_service, key_ceremony_service, key_ceremony_stage_base, key_ceremony_stages, key_ceremony_state_service, make_guardian, make_mediator, plaintext_ballot_service, public_key_to_dict, service_base, status_descriptions, to_ballot_share_raw, verification_to_dict, version_service, ) from electionguard_gui.start import ( run, ) __all__ = [ "AuthorizationService", "BallotUploadService", "ComponentBase", "ConfigurationService", "Container", "CreateDecryptionComponent", "CreateElectionComponent", "CreateKeyCeremonyComponent", "DB_HOST_KEY", "DB_PASSWORD_KEY", "DOCKER_MOUNT_DIR", "DbService", "DbWatcherService", "DecryptionDto", "DecryptionS1JoinService", "DecryptionS2AnnounceService", "DecryptionService", "DecryptionStageBase", "EelLogService", "ElectionDto", "ElectionListComponent", "ElectionService", "ExportElectionRecordComponent", "ExportEncryptionPackageComponent", "GuardianDecryptionShare", "GuardianHomeComponent", "GuardianService", "GuiSetupInputRetrievalStep", "HOST_KEY", "IS_ADMIN_KEY", "KeyCeremonyDetailsComponent", "KeyCeremonyDto", "KeyCeremonyS1JoinService", "KeyCeremonyS2AnnounceService", "KeyCeremonyS3MakeBackupService", "KeyCeremonyS4ShareBackupService", "KeyCeremonyS5VerifyBackupService", "KeyCeremonyS6PublishKeyService", "KeyCeremonyService", "KeyCeremonyStageBase", "KeyCeremonyStateService", "KeyCeremonyStates", "MODE_KEY", "MainApp", "PORT_KEY", "RetryException", "ServiceBase", "UploadBallotsComponent", "VersionService", "ViewDecryptionComponent", "ViewElectionComponent", "ViewSpoiledBallotComponent", "ViewTallyComponent", "announce_guardians", "authorization_service", "backup_to_dict", "ballot_upload_service", "component_base", "components", "configuration_service", "containers", "convert_utc_to_local", "create_decryption_component", "create_election_component", "create_key_ceremony_component", "db_serialization_service", "db_service", "db_watcher_service", "decryption_dto", "decryption_s1_join_service", "decryption_s2_announce_service", "decryption_service", "decryption_stage_base", "decryption_stages", "directory_service", "eel_fail", "eel_log_service", "eel_success", "eel_utils", "election_dto", "election_list_component", "election_service", "export_election_record_component", "export_encryption_package_component", "export_service", "get_data_dir", "get_export_dir", "get_export_locations", "get_guardian_number", "get_key_ceremony_status", "get_plaintext_ballot_report", "get_removable_drives", "get_spoiled_ballot_by_id", "get_tally", "guardian_home_component", "guardian_service", "gui_setup_input_retrieval_step", "joint_key_to_dict", "key_ceremony_details_component", "key_ceremony_dto", "key_ceremony_s1_join_service", "key_ceremony_s2_announce_service", "key_ceremony_s3_make_backup_service", "key_ceremony_s4_share_backup_service", "key_ceremony_s5_verify_backup_service", "key_ceremony_s6_publish_key_service", "key_ceremony_service", "key_ceremony_stage_base", "key_ceremony_stages", "key_ceremony_state_service", "key_ceremony_states", "main_app", "make_guardian", "make_mediator", "models", "notify_ui_db_changed", "plaintext_ballot_service", "public_key_to_dict", "refresh_decryption", "run", "service_base", "services", "start", "status_descriptions", "to_ballot_share_raw", "update_upload_status", "upload_ballots_component", "utc_to_str", "verification_to_dict", "version_service", "view_decryption_component", "view_election_component", "view_spoiled_ballot_component", "view_tally_component", ] ================================================ FILE: src/electionguard_gui/components/__init__.py ================================================ from electionguard_gui.components import component_base from electionguard_gui.components import create_decryption_component from electionguard_gui.components import create_election_component from electionguard_gui.components import create_key_ceremony_component from electionguard_gui.components import election_list_component from electionguard_gui.components import export_election_record_component from electionguard_gui.components import export_encryption_package_component from electionguard_gui.components import guardian_home_component from electionguard_gui.components import key_ceremony_details_component from electionguard_gui.components import upload_ballots_component from electionguard_gui.components import view_decryption_component from electionguard_gui.components import view_election_component from electionguard_gui.components import view_spoiled_ballot_component from electionguard_gui.components import view_tally_component from electionguard_gui.components.component_base import ( ComponentBase, ) from electionguard_gui.components.create_decryption_component import ( CreateDecryptionComponent, ) from electionguard_gui.components.create_election_component import ( CreateElectionComponent, ) from electionguard_gui.components.create_key_ceremony_component import ( CreateKeyCeremonyComponent, ) from electionguard_gui.components.election_list_component import ( ElectionListComponent, ) from electionguard_gui.components.export_election_record_component import ( ExportElectionRecordComponent, ) from electionguard_gui.components.export_encryption_package_component import ( ExportEncryptionPackageComponent, ) from electionguard_gui.components.guardian_home_component import ( GuardianHomeComponent, notify_ui_db_changed, ) from electionguard_gui.components.key_ceremony_details_component import ( KeyCeremonyDetailsComponent, ) from electionguard_gui.components.upload_ballots_component import ( UploadBallotsComponent, update_upload_status, ) from electionguard_gui.components.view_decryption_component import ( ViewDecryptionComponent, refresh_decryption, ) from electionguard_gui.components.view_election_component import ( ViewElectionComponent, ) from electionguard_gui.components.view_spoiled_ballot_component import ( ViewSpoiledBallotComponent, get_spoiled_ballot_by_id, ) from electionguard_gui.components.view_tally_component import ( ViewTallyComponent, ) __all__ = [ "ComponentBase", "CreateDecryptionComponent", "CreateElectionComponent", "CreateKeyCeremonyComponent", "ElectionListComponent", "ExportElectionRecordComponent", "ExportEncryptionPackageComponent", "GuardianHomeComponent", "KeyCeremonyDetailsComponent", "UploadBallotsComponent", "ViewDecryptionComponent", "ViewElectionComponent", "ViewSpoiledBallotComponent", "ViewTallyComponent", "component_base", "create_decryption_component", "create_election_component", "create_key_ceremony_component", "election_list_component", "export_election_record_component", "export_encryption_package_component", "get_spoiled_ballot_by_id", "guardian_home_component", "key_ceremony_details_component", "notify_ui_db_changed", "refresh_decryption", "update_upload_status", "upload_ballots_component", "view_decryption_component", "view_election_component", "view_spoiled_ballot_component", "view_tally_component", ] ================================================ FILE: src/electionguard_gui/components/component_base.py ================================================ from abc import ABC import traceback from typing import Any from electionguard_gui.eel_utils import eel_fail from electionguard_gui.services.db_service import DbService from electionguard_gui.services.eel_log_service import EelLogService class ComponentBase(ABC): """Responsible for common functionality among ell components""" _db_service: DbService _log: EelLogService def init( self, db_service: DbService, log_service: EelLogService, ) -> None: self._db_service = db_service self._log = log_service self.expose() def expose(self) -> None: """Override to expose the component's methods to JavaScript. This technique hides the fact that method names exposed must be globally unique.""" def handle_error(self, error: Exception) -> dict[str, Any]: self._log.error("error in component_base", error) traceback.print_exc() return eel_fail(str(error)) ================================================ FILE: src/electionguard_gui/components/create_decryption_component.py ================================================ from typing import Any import eel from electionguard_gui.eel_utils import eel_fail, eel_success from electionguard_gui.components.component_base import ComponentBase from electionguard_gui.models.election_dto import ElectionDto from electionguard_gui.services import ElectionService, DecryptionService class CreateDecryptionComponent(ComponentBase): """Responsible for functionality related to creating decryptions for an election""" _decryption_service: DecryptionService _election_service: ElectionService def __init__( self, decryption_service: DecryptionService, election_service: ElectionService, ) -> None: self._decryption_service = decryption_service self._election_service = election_service def expose(self) -> None: eel.expose(self.create_decryption) eel.expose(self.get_suggested_decryption_name) def get_suggested_decryption_name(self, election_id: str) -> dict[str, Any]: db = self._db_service.get_db() election: ElectionDto = self._election_service.get(db, election_id) existing_decryptions = self._decryption_service.get_decryption_count( db, election_id ) return eel_success( f"{election.election_name} Tally #{existing_decryptions + 1}" ) def create_decryption( self, election_id: str, decryption_name: str ) -> dict[str, Any]: try: self._log.debug( f"Creating decryption for election: {election_id} with name: {decryption_name}" ) db = self._db_service.get_db() election = self._election_service.get(db, election_id) if election is None: return eel_fail(f"Election {election_id} not found") name_exists = self._decryption_service.name_exists(db, decryption_name) if name_exists: return eel_fail(f"Decryption '{decryption_name}' already exists") decryption_id = self._decryption_service.create( db, election, decryption_name ) self._election_service.append_decryption( db, election_id, decryption_id, decryption_name ) return eel_success(decryption_id) # pylint: disable=broad-except except Exception as e: return self.handle_error(e) ================================================ FILE: src/electionguard_gui/components/create_election_component.py ================================================ from os import path from shutil import make_archive, rmtree from typing import Any import eel from electionguard.constants import get_constants from electionguard.guardian import Guardian from electionguard_cli.setup_election.output_setup_files_step import ( OutputSetupFilesStep, ) from electionguard_cli.setup_election.setup_election_builder_step import ( SetupElectionBuilderStep, ) from electionguard_gui.eel_utils import eel_fail, eel_success from electionguard_gui.components.component_base import ComponentBase from electionguard_gui.services import ( KeyCeremonyService, GuiSetupInputRetrievalStep, ElectionService, GuardianService, ) from electionguard_gui.services.directory_service import get_data_dir class CreateElectionComponent(ComponentBase): """Responsible for functionality related to creating encryption packages for elections""" _COMPRESSION_FORMAT = "zip" _key_ceremony_service: KeyCeremonyService _setup_input_retrieval_step: GuiSetupInputRetrievalStep _setup_election_builder_step: SetupElectionBuilderStep _election_service: ElectionService _output_setup_files_step: OutputSetupFilesStep _guardian_service: GuardianService def __init__( self, key_ceremony_service: KeyCeremonyService, election_service: ElectionService, setup_input_retrieval_step: GuiSetupInputRetrievalStep, setup_election_builder_step: SetupElectionBuilderStep, output_setup_files_step: OutputSetupFilesStep, guardian_service: GuardianService, ) -> None: self._key_ceremony_service = key_ceremony_service self._setup_input_retrieval_step = setup_input_retrieval_step self._setup_election_builder_step = setup_election_builder_step self._election_service = election_service self._output_setup_files_step = output_setup_files_step self._guardian_service = guardian_service def expose(self) -> None: eel.expose(self.get_keys) eel.expose(self.create_election) def get_keys(self) -> dict[str, Any]: self._log.debug("Getting keys") db = self._db_service.get_db() completed_key_ceremonies = self._key_ceremony_service.get_completed(db) keys = [ key_ceremony.to_id_name_dict() for key_ceremony in completed_key_ceremonies ] return eel_success(keys) def create_election( self, key_ceremony_id: str, election_name: str, manifest_raw: str, url: str ) -> dict[str, Any]: try: self._log.debug( f"Creating election key_ceremony_id: {key_ceremony_id}, " + f"election_name: {election_name}, " + f"url: {url}" ) db = self._db_service.get_db() existing_elections = db.elections.find_one({"election_name": election_name}) if existing_elections: fail_result: dict[str, Any] = eel_fail("Election already exists") return fail_result key_ceremony = self._key_ceremony_service.get(db, key_ceremony_id) guardians = [ Guardian.from_public_key( key_ceremony.guardian_count, key_ceremony.quorum, key ) for key in key_ceremony.keys ] election_inputs = self._setup_input_retrieval_step.get_gui_inputs( key_ceremony.guardian_count, key_ceremony.quorum, guardians, url, manifest_raw, ) joint_key = key_ceremony.get_joint_key() build_election_results = ( self._setup_election_builder_step.build_election_for_setup( election_inputs, joint_key ) ) temp_out_dir = path.join(get_data_dir(), "election_setup") self._output_setup_files_step.output( election_inputs, build_election_results, temp_out_dir, None ) zip_file = path.join( get_data_dir(), "encryption_packages", key_ceremony_id, "public_encryption_package", ) encryption_package_file = self._zip(temp_out_dir, zip_file) guardian_records = [ guardian.publish() for guardian in election_inputs.guardians ] constants = get_constants() election_id = self._election_service.create_election( db, election_name, key_ceremony, election_inputs.manifest, build_election_results.context, constants, guardian_records, encryption_package_file, url, ) return eel_success(election_id) # pylint: disable=broad-except except Exception as e: return self.handle_error(e) def _zip(self, dir_to_zip: str, zip_file_to_make: str) -> str: make_archive(zip_file_to_make, self._COMPRESSION_FORMAT, dir_to_zip) rmtree(dir_to_zip) self._log.debug(f"Temp zip file: {zip_file_to_make}.{self._COMPRESSION_FORMAT}") return f"{zip_file_to_make}.{self._COMPRESSION_FORMAT}" ================================================ FILE: src/electionguard_gui/components/create_key_ceremony_component.py ================================================ from typing import Any import eel from electionguard_gui.components.component_base import ComponentBase from electionguard_gui.eel_utils import eel_fail, eel_success from electionguard_gui.services.authorization_service import AuthorizationService from electionguard_gui.services.key_ceremony_service import KeyCeremonyService class CreateKeyCeremonyComponent(ComponentBase): """Responsible for functionality related to creating key ceremonies""" _key_ceremony_service: KeyCeremonyService _auth_service: AuthorizationService def __init__( self, key_ceremony_service: KeyCeremonyService, auth_service: AuthorizationService, ) -> None: super().__init__() self._key_ceremony_service = key_ceremony_service self._auth_service = auth_service def expose(self) -> None: eel.expose(self.create_key_ceremony) def create_key_ceremony( self, key_ceremony_name: str, guardian_count: int, quorum: int ) -> dict[str, Any]: if guardian_count < quorum: result: dict[str, Any] = eel_fail( "Guardian count must be greater than or equal to quorum" ) return result self._log.debug( "Starting ceremony: " + f"key_ceremony_name: {key_ceremony_name}, " + f"guardian_count: {guardian_count}, " + f"quorum: {quorum}" ) db = self._db_service.get_db() existing_key_ceremonies = self._key_ceremony_service.exists( db, key_ceremony_name ) if existing_key_ceremonies: self._log.debug(f"record '{key_ceremony_name}' already exists") fail_result: dict[str, Any] = eel_fail("Key ceremony name already exists") return fail_result inserted_id = self._key_ceremony_service.create( db, key_ceremony_name, guardian_count, quorum ) self._key_ceremony_service.notify_changed(db, inserted_id) result = eel_success(str(inserted_id)) return result ================================================ FILE: src/electionguard_gui/components/election_list_component.py ================================================ from typing import Any import eel from electionguard_gui.components.component_base import ComponentBase from electionguard_gui.eel_utils import eel_success from electionguard_gui.services import ElectionService class ElectionListComponent(ComponentBase): """Responsible for displaying multiple elections""" _election_service: ElectionService def __init__(self, election_service: ElectionService) -> None: self._election_service = election_service def expose(self) -> None: eel.expose(self.get_elections) def get_elections(self) -> dict[str, Any]: db = self._db_service.get_db() elections = self._election_service.get_all(db) elections_list = [election.to_id_name_dict() for election in elections] return eel_success(elections_list) ================================================ FILE: src/electionguard_gui/components/export_election_record_component.py ================================================ import os from tempfile import TemporaryDirectory from os.path import splitext from typing import Any from shutil import make_archive import eel from electionguard.constants import get_constants from electionguard_gui.eel_utils import eel_success from electionguard_gui.components.component_base import ComponentBase from electionguard_gui.services import ( ElectionService, DecryptionService, BallotUploadService, ) from electionguard_gui.services.export_service import get_export_locations from electionguard_tools.helpers.export import export_record class ExportElectionRecordComponent(ComponentBase): """Responsible for exporting an election record for an election""" _COMPRESSION_FORMAT = "zip" _election_service: ElectionService _decryption_service: DecryptionService _ballot_upload_service: BallotUploadService def __init__( self, election_service: ElectionService, decryption_service: DecryptionService, ballot_upload_service: BallotUploadService, ) -> None: self._election_service = election_service self._decryption_service = decryption_service self._ballot_upload_service = ballot_upload_service def expose(self) -> None: eel.expose(self.export_election_record) eel.expose(self.get_election_record_export_locations) def get_election_record_export_locations(self) -> dict[str, Any]: self._log.trace("getting export locations") export_locations = get_export_locations() locations = [ os.path.join(location, "publish-election.zip") for location in export_locations ] return eel_success(locations) def export_election_record( self, decryption_id: str, location: str ) -> dict[str, Any]: db = self._db_service.get_db() self._log.debug(f"exporting election record {decryption_id} to {location}") decryption = self._decryption_service.get(db, decryption_id) election = self._election_service.get(db, decryption.election_id) context = election.get_context() manifest = election.get_manifest() constants = get_constants() encryption_devices = election.get_encryption_devices() submitted_ballots = self._ballot_upload_service.get_ballots( db, election.id, lambda x: None ) plaintext_tally = decryption.get_plaintext_tally() spoiled_ballots = decryption.get_plaintext_spoiled_ballots() lagrange_coefficients = decryption.get_lagrange_coefficients() ciphertext_tally = decryption.get_ciphertext_tally() guardian_records = election.get_guardian_records() with TemporaryDirectory() as temp_dir: export_record( manifest, context, constants, encryption_devices, submitted_ballots, spoiled_ballots, ciphertext_tally, plaintext_tally, guardian_records, lagrange_coefficients, election_record_directory=temp_dir, ) file_name = splitext(location)[0] make_archive(file_name, self._COMPRESSION_FORMAT, temp_dir) return eel_success() ================================================ FILE: src/electionguard_gui/components/export_encryption_package_component.py ================================================ import os from typing import Any from shutil import unpack_archive import eel from electionguard_gui.eel_utils import eel_success from electionguard_gui.components.component_base import ComponentBase from electionguard_gui.services import ElectionService from electionguard_gui.services.export_service import get_export_locations class ExportEncryptionPackageComponent(ComponentBase): """Responsible for exporting an encryption package for an election""" _election_service: ElectionService def __init__(self, election_service: ElectionService) -> None: self._election_service = election_service def expose(self) -> None: eel.expose(self.get_encryption_package_export_locations) eel.expose(self.export_encryption_package) def get_encryption_package_export_locations(self) -> dict[str, Any]: self._log.trace("getting export locations") export_locations = get_export_locations() artifacts_locations = [ os.path.join(location, "artifacts") for location in export_locations ] return eel_success(artifacts_locations) def export_encryption_package( self, election_id: str, location: str ) -> dict[str, Any]: db = self._db_service.get_db() election = self._election_service.get(db, election_id) if not election.encryption_package_file: raise Exception("No encryption package file") self._log.debug(f"unzipping: {election.encryption_package_file} to {location}") unpack_archive(election.encryption_package_file, location) return eel_success() ================================================ FILE: src/electionguard_gui/components/guardian_home_component.py ================================================ from typing import Any import eel # type: ignore[import-untyped] from electionguard_gui.eel_utils import eel_success from electionguard_gui.components.component_base import ComponentBase from electionguard_gui.services import ( KeyCeremonyService, DecryptionService, DbWatcherService, ) class GuardianHomeComponent(ComponentBase): """Responsible for functionality related to the guardian home page""" _key_ceremony_service: KeyCeremonyService _decryption_service: DecryptionService _db_watcher_service: DbWatcherService def __init__( self, key_ceremony_service: KeyCeremonyService, decryption_service: DecryptionService, db_watcher_service: DbWatcherService, ) -> None: super().__init__() self._key_ceremony_service = key_ceremony_service self._decryption_service = decryption_service self._db_watcher_service = db_watcher_service def expose(self) -> None: eel.expose(self.get_decryptions) eel.expose(self.get_key_ceremonies) eel.expose(self.watch_db_collections) eel.expose(self.stop_watching_db_collections) def get_decryptions(self) -> dict[str, Any]: db = self._db_service.get_db() decryptions = self._decryption_service.get_active(db) decryptions_json = [decryption.to_id_name_dict() for decryption in decryptions] return eel_success(decryptions_json) def get_key_ceremonies(self) -> dict[str, Any]: db = self._db_service.get_db() key_ceremonies = self._key_ceremony_service.get_active(db) js_key_ceremonies = [ key_ceremony.to_id_name_dict() for key_ceremony in key_ceremonies ] return eel_success(js_key_ceremonies) def watch_db_collections(self) -> None: try: self._log.debug("Watching database") db = self._db_service.get_db() self._db_watcher_service.watch_database(db, None, notify_ui_db_changed) self._log.debug("exited watching database") except KeyboardInterrupt: self._log.debug("Keyboard interrupt, exiting watch database") self._db_watcher_service.stop_watching() except Exception as e: # pylint: disable=broad-except self.handle_error(e) self._db_watcher_service.stop_watching() # no need to raise exception or return anything, we're in fire-and-forget mode here def stop_watching_db_collections(self) -> None: self._log.debug("Stopping watch database") self._db_watcher_service.stop_watching() def notify_ui_db_changed(collection: str, _: str) -> None: # pylint: disable=no-member if collection == "key_ceremonies": eel.key_ceremonies_changed() # type: ignore[attr-defined] if collection == "decryptions": eel.decryptions_changed() # type: ignore[attr-defined] ================================================ FILE: src/electionguard_gui/components/key_ceremony_details_component.py ================================================ import traceback from typing import List import eel # type: ignore[import-untyped] from pymongo.database import Database from electionguard_gui.eel_utils import eel_fail, eel_success from electionguard_gui.services.key_ceremony_stages import ( KeyCeremonyStageBase, KeyCeremonyS1JoinService, KeyCeremonyS2AnnounceService, KeyCeremonyS3MakeBackupService, KeyCeremonyS4ShareBackupService, KeyCeremonyS5VerifyBackupService, KeyCeremonyS6PublishKeyService, ) from electionguard_gui.models.key_ceremony_dto import KeyCeremonyDto from electionguard_gui.services.key_ceremony_state_service import ( KeyCeremonyStateService, get_key_ceremony_status, ) from electionguard_gui.services import ( AuthorizationService, DbWatcherService, KeyCeremonyService, ) from electionguard_gui.components.component_base import ComponentBase class KeyCeremonyDetailsComponent(ComponentBase): """Responsible for retrieving key ceremony details""" _auth_service: AuthorizationService _ceremony_state_service: KeyCeremonyStateService _db_watcher_service: DbWatcherService _key_ceremony_s1_join_service: KeyCeremonyS1JoinService key_ceremony_watch_stages: List[KeyCeremonyStageBase] def __init__( self, key_ceremony_service: KeyCeremonyService, auth_service: AuthorizationService, db_watcher_service: DbWatcherService, key_ceremony_state_service: KeyCeremonyStateService, key_ceremony_s1_join_service: KeyCeremonyS1JoinService, key_ceremony_s2_announce_service: KeyCeremonyS2AnnounceService, key_ceremony_s3_make_backup_service: KeyCeremonyS3MakeBackupService, key_ceremony_s4_share_backup_service: KeyCeremonyS4ShareBackupService, key_ceremony_s5_verification_service: KeyCeremonyS5VerifyBackupService, key_ceremony_s6_publish_key_service: KeyCeremonyS6PublishKeyService, ) -> None: super().__init__() self._key_ceremony_service = key_ceremony_service self._ceremony_state_service = key_ceremony_state_service self._auth_service = auth_service self._db_watcher_service = db_watcher_service self._key_ceremony_s1_join_service = key_ceremony_s1_join_service self.key_ceremony_watch_stages = [ key_ceremony_s2_announce_service, key_ceremony_s3_make_backup_service, key_ceremony_s4_share_backup_service, key_ceremony_s5_verification_service, key_ceremony_s6_publish_key_service, ] def expose(self) -> None: eel.expose(self.join_key_ceremony) eel.expose(self.watch_key_ceremony) eel.expose(self.stop_watching_key_ceremony) def watch_key_ceremony(self, key_ceremony_id: str) -> None: try: db = self._db_service.get_db() # retrieve and send the key ceremony to the client self.on_key_ceremony_changed("key_ceremonies", key_ceremony_id) self._log.debug(f"watching key ceremony '{key_ceremony_id}'") # start watching for key ceremony changes from guardians self._db_watcher_service.watch_database( db, key_ceremony_id, self.on_key_ceremony_changed ) except KeyboardInterrupt: self._log.debug("Keyboard interrupt, exiting watch database") self._db_watcher_service.stop_watching() except Exception as e: # pylint: disable=broad-except self.handle_error(e) self._db_watcher_service.stop_watching() # we're in a fire-and-forget scenario, so no need to raise an exception or return anything def stop_watching_key_ceremony(self) -> None: self._db_watcher_service.stop_watching() def on_key_ceremony_changed(self, _: str, key_ceremony_id: str) -> None: try: self._log.debug( f"on_key_ceremony_changed key_ceremony_id: '{key_ceremony_id}'" ) db = self._db_service.get_db() key_ceremony = self.get_ceremony(db, key_ceremony_id) state = self._ceremony_state_service.get_key_ceremony_state(key_ceremony) self._log.debug(f"{key_ceremony_id} state = '{state}'") for stage in self.key_ceremony_watch_stages: if stage.should_run(key_ceremony, state): stage.run(db, key_ceremony) break key_ceremony = self.get_ceremony(db, key_ceremony_id) new_state = self._ceremony_state_service.get_key_ceremony_state( key_ceremony ) if state != new_state: self._log.debug(f"state changed from {state} to {new_state}") key_ceremony.status = get_key_ceremony_status(new_state) result = key_ceremony.to_dict() # pylint: disable=no-member eel.refresh_key_ceremony(eel_success(result)) # type: ignore[attr-defined] # pylint: disable=broad-except except Exception as e: self._log.error("error on key ceremony changed", e) traceback.print_exc() # pylint: disable=no-member eel.refresh_key_ceremony(eel_fail(str(e))) # type: ignore[attr-defined] def join_key_ceremony(self, key_ceremony_id: str) -> None: try: db = self._db_service.get_db() key_ceremony = self.get_ceremony(db, key_ceremony_id) self._key_ceremony_s1_join_service.run(db, key_ceremony) # pylint: disable=broad-except except Exception as e: self.handle_error(e) def get_ceremony(self, db: Database, id: str) -> KeyCeremonyDto: key_ceremony = self._key_ceremony_service.get(db, id) return key_ceremony ================================================ FILE: src/electionguard_gui/components/upload_ballots_component.py ================================================ import os from typing import Any from datetime import datetime, timezone import eel # type: ignore[import-untyped] from electionguard.encrypt import EncryptionDevice from electionguard.serialize import from_file, from_raw from electionguard.ballot import SubmittedBallot from electionguard_gui.components.component_base import ComponentBase from electionguard_gui.eel_utils import eel_fail, eel_success from electionguard_gui.services import ElectionService, BallotUploadService from electionguard_gui.services.export_service import get_removable_drives class UploadBallotsComponent(ComponentBase): """Responsible for uploading ballots to an election via the GUI""" _election_service: ElectionService _ballot_upload_service: BallotUploadService def __init__( self, election_service: ElectionService, ballot_upload_service: BallotUploadService, ) -> None: self._election_service = election_service self._ballot_upload_service = ballot_upload_service def expose(self) -> None: eel.expose(self.create_ballot_upload) eel.expose(self.upload_ballot) eel.expose(self.is_wizard_supported) eel.expose(self.scan_drives) eel.expose(self.upload_ballots) def create_ballot_upload( self, election_id: str, device_file_name: str, device_file_contents: str, ) -> dict[str, Any]: try: db = self._db_service.get_db() self._log.debug(f"creating upload for {election_id}") election = self._election_service.get(db, election_id) if election is None: return eel_fail(f"Election {election_id} not found") created_at = datetime.now(timezone.utc) ballot_upload_id = self._ballot_upload_service.create( db, election_id, device_file_name, device_file_contents, created_at, ) self._election_service.append_ballot_upload( db, election_id, ballot_upload_id, device_file_contents, created_at, ) return eel_success(ballot_upload_id) # pylint: disable=broad-except except Exception as e: return self.handle_error(e) def upload_ballot( self, ballot_upload_id: str, election_id: str, file_name: str, file_contents: str, ) -> dict[str, Any]: try: db = self._db_service.get_db() self._log.trace(f"adding ballot {file_name} to {ballot_upload_id}") ballot = from_raw(SubmittedBallot, file_contents) election = self._election_service.get(db, election_id) context = election.get_context() if context.manifest_hash != ballot.manifest_hash: self._log.warn( f"ballot '{ballot.object_id}' had a mismatched manifest hash. " + f"Expected {context.manifest_hash}, got {ballot.manifest_hash}." ) return eel_fail( "The uploaded ballot didn't match the encryption package for this election. " + "Please try a different ballot." ) is_duplicate = self._ballot_upload_service.any_ballot_exists( db, election_id, ballot.object_id ) if is_duplicate: self._log.warn( "ballot '{ballot.object_id}' already exists in election '{election_id}'" ) return eel_success({"is_duplicate": True}) success = self._ballot_upload_service.add_ballot( db, ballot_upload_id, election_id, file_name, file_contents, ballot.object_id, ) if success: self._ballot_upload_service.increment_ballot_count(db, ballot_upload_id) self._election_service.increment_ballot_upload_ballot_count( db, election_id, ballot_upload_id ) return eel_success({"is_duplicate": False}) # pylint: disable=broad-except except Exception as e: return self.handle_error(e) def is_wizard_supported(self) -> bool: on_windows = os.name == "nt" return on_windows def scan_drives(self) -> dict[str, Any]: try: removable_drives = get_removable_drives() self._log.trace(f"found {len(removable_drives)} removable drives") candidate_drives = [ self.parse_drive(drive) for drive in removable_drives if os.path.exists(os.path.join(drive, "artifacts", "encrypted_ballots")) and os.path.exists(os.path.join(drive, "artifacts", "devices")) ] first_candidate = next(iter(candidate_drives), None) return eel_success(first_candidate) # pylint: disable=broad-except except Exception as e: return self.handle_error(e) def parse_drive(self, drive: str) -> dict[str, Any]: ballots_dir = os.path.join(drive, "artifacts", "encrypted_ballots") devices_dir = os.path.join(drive, "artifacts", "devices") device_files = os.listdir(devices_dir) device_file_name = next(iter(os.listdir(devices_dir))) device_file_path = os.path.join(devices_dir, device_file_name) if len(device_files) > 1: self._log.warn( "found multiple device files in drive, using " + device_file_name ) device_file_json = from_file(EncryptionDevice, device_file_path) location = device_file_json.location ballot_count = len(os.listdir(ballots_dir)) return { "drive": drive, "ballots": ballot_count, "location": location, "device_file_name": device_file_name, "device_file_path": device_file_path, "ballots_dir": ballots_dir, } def upload_ballots(self, election_id: str) -> dict[str, Any]: try: update_upload_status("Scanning drives") drive_info = self.scan_drives() device_file_name = drive_info["result"]["device_file_name"] device_file_path = drive_info["result"]["device_file_path"] self._log.debug( f"uploading ballots for {election_id} from {device_file_path} device {device_file_name}" ) update_upload_status("Uploading device file") ballot_upload_result = self.create_ballot_upload_from_file( election_id, device_file_name, device_file_path, ) if not ballot_upload_result["success"]: return ballot_upload_result ballots_dir: str = drive_info["result"]["ballots_dir"] ballot_files = os.listdir(ballots_dir) ballot_upload_id: str = ballot_upload_result["result"] ballot_num = 1 duplicate_count = 0 ballot_count = len(ballot_files) for ballot_file in ballot_files: self._log.debug("uploading ballot " + ballot_file) update_upload_status(f"Uploading ballot {ballot_num}/{ballot_count}") result = self.create_ballot_from_file( election_id, ballot_file, ballot_upload_id, ballots_dir ) if not result["success"]: return result if result["result"]["is_duplicate"]: duplicate_count += 1 ballot_num += 1 return eel_success( {"ballot_count": ballot_count, "duplicate_count": duplicate_count} ) # pylint: disable=broad-except except Exception as e: return self.handle_error(e) def create_ballot_from_file( self, election_id: str, ballot_file_name: str, ballot_upload_id: str, ballots_dir: str, ) -> dict[str, Any]: ballot_file_path = os.path.join(ballots_dir, ballot_file_name) with open(ballot_file_path, "r", encoding="utf-8") as ballot_file: ballot_contents = ballot_file.read() return self.upload_ballot( ballot_upload_id, election_id, ballot_file_name, ballot_contents ) def create_ballot_upload_from_file( self, election_id: str, device_file_name: str, device_file_path: str ) -> dict[str, Any]: with open(device_file_path, "r", encoding="utf-8") as device_file: ballot_upload = self.create_ballot_upload( election_id, device_file_name, device_file.read() ) return ballot_upload def update_upload_status(status: str) -> None: # pylint: disable=no-member eel.update_upload_status(status) # type: ignore[attr-defined] ================================================ FILE: src/electionguard_gui/components/view_decryption_component.py ================================================ import traceback from typing import Any import eel # type: ignore[import-untyped] from pymongo.database import Database from electionguard_gui.eel_utils import eel_fail, eel_success from electionguard_gui.components.component_base import ComponentBase from electionguard_gui.models.decryption_dto import DecryptionDto from electionguard_gui.services import ( ElectionService, DecryptionService, DbWatcherService, ) from electionguard_gui.services.decryption_stages import ( DecryptionS1JoinService, DecryptionS2AnnounceService, ) class ViewDecryptionComponent(ComponentBase): """Responsible for functionality related to creating decryptions for an election""" _decryption_service: DecryptionService _election_service: ElectionService _decryption_s1_join_service: DecryptionS1JoinService _decryption_s2_announce_service: DecryptionS2AnnounceService _db_watcher_service: DbWatcherService def __init__( self, decryption_service: DecryptionService, election_service: ElectionService, decryption_s1_join_service: DecryptionS1JoinService, decryption_s2_announce_service: DecryptionS2AnnounceService, db_watcher_service: DbWatcherService, ) -> None: self._decryption_service = decryption_service self._election_service = election_service self._decryption_s1_join_service = decryption_s1_join_service self._decryption_s2_announce_service = decryption_s2_announce_service self._db_watcher_service = db_watcher_service def expose(self) -> None: eel.expose(self.get_decryption) eel.expose(self.watch_decryption) eel.expose(self.stop_watching_decryption) eel.expose(self.join_decryption) def watch_decryption(self, decryption_id: str) -> None: try: db = self._db_service.get_db() self._log.debug(f"watching decryption '{decryption_id}'") self._db_watcher_service.watch_database( db, decryption_id, self.on_decryption_changed ) except Exception as e: # pylint: disable=broad-except self.handle_error(e) self._db_watcher_service.stop_watching() # no need to raise exception or return anything, we're in fire-and-forget mode here def stop_watching_decryption(self) -> None: self._db_watcher_service.stop_watching() def on_decryption_changed(self, _: str, decryption_id: str) -> None: try: self._log.debug(f"on_key_ceremony_changed decryption_id: '{decryption_id}'") db = self._db_service.get_db() decryption = self._decryption_service.get(db, decryption_id) self.try_run_stage_2(db, decryption) refresh_decryption(eel_success()) # pylint: disable=broad-except except Exception as e: self._log.error("error in on decryption changed", e) traceback.print_exc() refresh_decryption(eel_fail(str(e))) def try_run_stage_2(self, db: Database, decryption: DecryptionDto) -> bool: if self._decryption_s2_announce_service.should_run(db, decryption): refresh_decryption(eel_success()) # give the UI a chance to update eel.sleep(0.5) self._decryption_s2_announce_service.run(db, decryption) return True return False def get_decryption(self, decryption_id: str, is_refresh: bool) -> dict[str, Any]: try: db = self._db_service.get_db() decryption = self._decryption_service.get(db, decryption_id) if not is_refresh: did_run = self.try_run_stage_2(db, decryption) if did_run: decryption = self._decryption_service.get(db, decryption_id) return eel_success(decryption.to_dict()) # pylint: disable=broad-except except Exception as e: return self.handle_error(e) def join_decryption(self, decryption_id: str) -> dict[str, Any]: try: db = self._db_service.get_db() decryption = self._decryption_service.get(db, decryption_id) self._decryption_s1_join_service.run(db, decryption) return eel_success(decryption.to_dict()) # pylint: disable=broad-except except Exception as e: return self.handle_error(e) def refresh_decryption(result: dict[str, Any]) -> None: # pylint: disable=no-member eel.refresh_decryption(result) # type: ignore[attr-defined] ================================================ FILE: src/electionguard_gui/components/view_election_component.py ================================================ from typing import Any import eel from electionguard_gui.components.component_base import ComponentBase from electionguard_gui.eel_utils import eel_success from electionguard_gui.services import ElectionService class ViewElectionComponent(ComponentBase): """Responsible for viewing election details""" _election_service: ElectionService def __init__(self, election_service: ElectionService) -> None: self._election_service = election_service def expose(self) -> None: eel.expose(self.get_election) def get_election(self, election_id: str) -> dict[str, Any]: db = self._db_service.get_db() election = self._election_service.get(db, election_id) return eel_success(election.to_dict()) ================================================ FILE: src/electionguard_gui/components/view_spoiled_ballot_component.py ================================================ from typing import Any import eel from electionguard.tally import PlaintextTally from electionguard_gui.eel_utils import eel_success from electionguard_gui.components.component_base import ComponentBase from electionguard_gui.services import ( DecryptionService, ElectionService, ) from electionguard_gui.services.plaintext_ballot_service import ( get_plaintext_ballot_report, ) class ViewSpoiledBallotComponent(ComponentBase): """Responsible for functionality related to viewing a tally""" _decryption_service: DecryptionService _election_service: ElectionService def __init__( self, decryption_service: DecryptionService, election_service: ElectionService ) -> None: self._decryption_service = decryption_service self._election_service = election_service def expose(self) -> None: eel.expose(self.get_spoiled_ballot) def get_spoiled_ballot( self, decryption_id: str, spoiled_ballot_id: str ) -> dict[str, Any]: try: db = self._db_service.get_db() self._log.debug( f"retrieving spoiled ballot '{decryption_id}'.{spoiled_ballot_id}" ) decryption = self._decryption_service.get(db, decryption_id) election = self._election_service.get(db, decryption.election_id) spoiled_ballots = decryption.get_plaintext_spoiled_ballots() plaintext_tally = get_spoiled_ballot_by_id( spoiled_ballots, spoiled_ballot_id ) tally_report = get_plaintext_ballot_report(election, plaintext_tally) result = { "election_id": election.id, "election_name": election.election_name, "decryption_name": decryption.decryption_name, "report": tally_report, } return eel_success(result) # pylint: disable=broad-except except Exception as e: return self.handle_error(e) def get_spoiled_ballot_by_id( spoiled_ballots: list[PlaintextTally], spoiled_ballot_id: str ) -> PlaintextTally: matches: list[PlaintextTally] = [ ballot for ballot in spoiled_ballots if ballot.object_id == spoiled_ballot_id ] return next(iter(matches)) ================================================ FILE: src/electionguard_gui/components/view_tally_component.py ================================================ from typing import Any import eel from electionguard_gui.eel_utils import eel_success from electionguard_gui.components.component_base import ComponentBase from electionguard_gui.services import ( DecryptionService, ElectionService, ) from electionguard_gui.services.plaintext_ballot_service import ( get_plaintext_ballot_report, ) class ViewTallyComponent(ComponentBase): """Responsible for functionality related to viewing a tally""" _decryption_service: DecryptionService _election_service: ElectionService def __init__( self, decryption_service: DecryptionService, election_service: ElectionService ) -> None: self._decryption_service = decryption_service self._election_service = election_service def expose(self) -> None: eel.expose(self.get_tally) def get_tally(self, decryption_id: str) -> dict[str, Any]: try: db = self._db_service.get_db() self._log.debug(f"retrieving decryption '{decryption_id}'") decryption = self._decryption_service.get(db, decryption_id) election = self._election_service.get(db, decryption.election_id) plaintext_tally = decryption.get_plaintext_tally() tally_report = get_plaintext_ballot_report(election, plaintext_tally) result = { "election_id": election.id, "election_name": election.election_name, "decryption_name": decryption.decryption_name, "report": tally_report, } return eel_success(result) # pylint: disable=broad-except except Exception as e: return self.handle_error(e) ================================================ FILE: src/electionguard_gui/containers.py ================================================ from dependency_injector import containers, providers from dependency_injector.providers import Factory, Singleton from electionguard_cli.setup_election.output_setup_files_step import ( OutputSetupFilesStep, ) from electionguard_cli.setup_election.setup_election_builder_step import ( SetupElectionBuilderStep, ) from electionguard_gui.components import ( CreateElectionComponent, ViewElectionComponent, CreateKeyCeremonyComponent, ElectionListComponent, GuardianHomeComponent, KeyCeremonyDetailsComponent, ExportEncryptionPackageComponent, UploadBallotsComponent, CreateDecryptionComponent, ViewDecryptionComponent, ExportElectionRecordComponent, ViewTallyComponent, ViewSpoiledBallotComponent, ) from electionguard_gui.main_app import MainApp from electionguard_gui.services import ( AuthorizationService, DbService, EelLogService, ElectionService, GuardianService, KeyCeremonyService, KeyCeremonyStateService, GuiSetupInputRetrievalStep, BallotUploadService, DecryptionService, DbWatcherService, ConfigurationService, VersionService, ) from electionguard_gui.services.decryption_stages import ( DecryptionS1JoinService, DecryptionS2AnnounceService, ) from electionguard_gui.services.key_ceremony_stages import ( KeyCeremonyS1JoinService, KeyCeremonyS2AnnounceService, KeyCeremonyS3MakeBackupService, KeyCeremonyS4ShareBackupService, KeyCeremonyS5VerifyBackupService, KeyCeremonyS6PublishKeyService, ) class Container(containers.DeclarativeContainer): """Responsible for dependency injection and how components are wired together""" # services log_service: Singleton[EelLogService] = providers.Singleton(EelLogService) config_service: Factory[ConfigurationService] = providers.Factory( ConfigurationService ) version_service: Factory[VersionService] = providers.Factory( VersionService, log_service=log_service ) db_service: Singleton[DbService] = providers.Singleton( DbService, log_service=log_service, config_service=config_service ) authorization_service: Singleton[AuthorizationService] = providers.Singleton( AuthorizationService, config_service=config_service ) db_watcher_service: Factory[DbWatcherService] = providers.Factory( DbWatcherService, log_service=log_service ) key_ceremony_service: Factory[KeyCeremonyService] = providers.Factory( KeyCeremonyService, log_service=log_service, auth_service=authorization_service, db_watcher_service=db_watcher_service, ) election_service: Factory[ElectionService] = providers.Factory( ElectionService, log_service=log_service, auth_service=authorization_service ) key_ceremony_state_service: Factory[KeyCeremonyStateService] = providers.Factory( KeyCeremonyStateService, log_service=log_service ) guardian_service: Factory[GuardianService] = providers.Factory( GuardianService, log_service=log_service ) setup_input_retrieval_step: Factory[GuiSetupInputRetrievalStep] = providers.Factory( GuiSetupInputRetrievalStep ) setup_election_builder_step: Factory[SetupElectionBuilderStep] = providers.Factory( SetupElectionBuilderStep ) output_setup_files_step: Factory[OutputSetupFilesStep] = providers.Factory( OutputSetupFilesStep ) ballot_upload_service: Factory[BallotUploadService] = providers.Factory( BallotUploadService, log_service=log_service, auth_service=authorization_service ) decryption_service: Factory[DecryptionService] = providers.Factory( DecryptionService, log_service=log_service, auth_service=authorization_service, db_watcher_service=db_watcher_service, ) # decryption services decryption_s1_join_service: Factory[DecryptionS1JoinService] = providers.Factory( DecryptionS1JoinService, log_service=log_service, db_service=db_service, decryption_service=decryption_service, auth_service=authorization_service, guardian_service=guardian_service, ballot_upload_service=ballot_upload_service, election_service=election_service, ) decryption_s2_announce_service: Factory[DecryptionS2AnnounceService] = ( providers.Factory( DecryptionS2AnnounceService, log_service=log_service, db_service=db_service, decryption_service=decryption_service, auth_service=authorization_service, guardian_service=guardian_service, ballot_upload_service=ballot_upload_service, election_service=election_service, ) ) # key ceremony services key_ceremony_s1_join_service: Factory[KeyCeremonyS1JoinService] = providers.Factory( KeyCeremonyS1JoinService, log_service=log_service, db_service=db_service, key_ceremony_service=key_ceremony_service, auth_service=authorization_service, key_ceremony_state_service=key_ceremony_state_service, guardian_service=guardian_service, ) key_ceremony_s2_announce_service: Factory[KeyCeremonyS2AnnounceService] = ( providers.Factory( KeyCeremonyS2AnnounceService, log_service=log_service, db_service=db_service, key_ceremony_service=key_ceremony_service, auth_service=authorization_service, key_ceremony_state_service=key_ceremony_state_service, guardian_service=guardian_service, ) ) key_ceremony_s3_make_backup_service: Factory[KeyCeremonyS3MakeBackupService] = ( providers.Factory( KeyCeremonyS3MakeBackupService, log_service=log_service, db_service=db_service, key_ceremony_service=key_ceremony_service, auth_service=authorization_service, key_ceremony_state_service=key_ceremony_state_service, guardian_service=guardian_service, ) ) key_ceremony_s4_share_backup_service: Factory[KeyCeremonyS4ShareBackupService] = ( providers.Factory( KeyCeremonyS4ShareBackupService, log_service=log_service, db_service=db_service, key_ceremony_service=key_ceremony_service, auth_service=authorization_service, key_ceremony_state_service=key_ceremony_state_service, guardian_service=guardian_service, ) ) key_ceremony_s5_verification_service: Factory[KeyCeremonyS5VerifyBackupService] = ( providers.Factory( KeyCeremonyS5VerifyBackupService, log_service=log_service, db_service=db_service, key_ceremony_service=key_ceremony_service, auth_service=authorization_service, key_ceremony_state_service=key_ceremony_state_service, guardian_service=guardian_service, ) ) key_ceremony_s6_publish_key_service: Factory[KeyCeremonyS6PublishKeyService] = ( providers.Factory( KeyCeremonyS6PublishKeyService, log_service=log_service, db_service=db_service, key_ceremony_service=key_ceremony_service, auth_service=authorization_service, key_ceremony_state_service=key_ceremony_state_service, guardian_service=guardian_service, ) ) # components guardian_home_component: Factory[GuardianHomeComponent] = providers.Factory( GuardianHomeComponent, key_ceremony_service=key_ceremony_service, decryption_service=decryption_service, db_watcher_service=db_watcher_service, ) create_election_component: Factory[CreateElectionComponent] = providers.Factory( CreateElectionComponent, key_ceremony_service=key_ceremony_service, election_service=election_service, setup_election_builder_step=setup_election_builder_step, setup_input_retrieval_step=setup_input_retrieval_step, output_setup_files_step=output_setup_files_step, guardian_service=guardian_service, ) create_key_ceremony_component: Factory[CreateKeyCeremonyComponent] = ( providers.Factory( CreateKeyCeremonyComponent, key_ceremony_service=key_ceremony_service, auth_service=authorization_service, ) ) election_list_component: Factory[ElectionListComponent] = providers.Factory( ElectionListComponent, election_service=election_service, ) view_election_component: Factory[ViewElectionComponent] = providers.Factory( ViewElectionComponent, election_service=election_service, ) key_ceremony_details_component: Factory[KeyCeremonyDetailsComponent] = ( providers.Factory( KeyCeremonyDetailsComponent, key_ceremony_service=key_ceremony_service, auth_service=authorization_service, db_watcher_service=db_watcher_service, key_ceremony_state_service=key_ceremony_state_service, key_ceremony_s1_join_service=key_ceremony_s1_join_service, key_ceremony_s2_announce_service=key_ceremony_s2_announce_service, key_ceremony_s3_make_backup_service=key_ceremony_s3_make_backup_service, key_ceremony_s4_share_backup_service=key_ceremony_s4_share_backup_service, key_ceremony_s5_verification_service=key_ceremony_s5_verification_service, key_ceremony_s6_publish_key_service=key_ceremony_s6_publish_key_service, ) ) export_encryption_package: Factory[ExportEncryptionPackageComponent] = ( providers.Factory( ExportEncryptionPackageComponent, election_service=election_service, ) ) upload_ballots_component: Factory[UploadBallotsComponent] = providers.Factory( UploadBallotsComponent, election_service=election_service, ballot_upload_service=ballot_upload_service, ) create_decryption_component: Factory[CreateDecryptionComponent] = providers.Factory( CreateDecryptionComponent, election_service=election_service, decryption_service=decryption_service, ) view_decryption_component: Factory[ViewDecryptionComponent] = providers.Factory( ViewDecryptionComponent, election_service=election_service, decryption_service=decryption_service, decryption_s1_join_service=decryption_s1_join_service, decryption_s2_announce_service=decryption_s2_announce_service, db_watcher_service=db_watcher_service, ) export_election_record_component: Factory[ExportElectionRecordComponent] = ( providers.Factory( ExportElectionRecordComponent, election_service=election_service, decryption_service=decryption_service, ballot_upload_service=ballot_upload_service, ) ) view_tally_component: Factory[ViewTallyComponent] = providers.Factory( ViewTallyComponent, decryption_service=decryption_service, election_service=election_service, ) view_spoiled_ballot_component: Factory[ViewSpoiledBallotComponent] = ( providers.Factory( ViewSpoiledBallotComponent, decryption_service=decryption_service, election_service=election_service, ) ) # main main_app: Factory[MainApp] = providers.Factory( MainApp, log_service=log_service, config_service=config_service, db_service=db_service, guardian_home_component=guardian_home_component, create_key_ceremony_component=create_key_ceremony_component, key_ceremony_details_component=key_ceremony_details_component, authorization_service=authorization_service, create_election_component=create_election_component, view_election_component=view_election_component, election_list_component=election_list_component, export_encryption_package=export_encryption_package, upload_ballots_component=upload_ballots_component, create_decryption_component=create_decryption_component, view_decryption_component=view_decryption_component, export_election_record_component=export_election_record_component, view_tally_component=view_tally_component, view_spoiled_ballot_component=view_spoiled_ballot_component, version_service=version_service, ) ================================================ FILE: src/electionguard_gui/docker-compose.yml ================================================ version: "3.8" services: mongo: image: mongo:4.4 container_name: "electionguard-db" restart: always ports: - 27017:27017 environment: MONGO_INITDB_ROOT_USERNAME: root MONGO_INITDB_ROOT_PASSWORD: ${EG_DB_PASSWORD} MONGO_INITDB_DATABASE: ElectionGuardDb volumes: - ../electionguard_db/mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js:ro - ${EG_DB_DIR}:/data/db mongo-express: image: mongo-express:1.0.0-alpha.4 restart: always ports: - 8181:8081 environment: ME_CONFIG_MONGODB_SERVER: electionguard-db ME_CONFIG_MONGODB_ADMINUSERNAME: root ME_CONFIG_MONGODB_ADMINPASSWORD: ${EG_DB_PASSWORD} depends_on: - mongo volumes: - ${EG_DB_DIR}:/data/db admin1: image: egui:latest container_name: "admin1" restart: always ports: - 12801:12800 environment: EG_DB_PASSWORD: ${EG_DB_PASSWORD} EG_DB_HOST: mongo EG_PORT: 12800 EG_MODE: none EG_HOST: 0.0.0.0 EG_IS_ADMIN: True # to get logs via `docker logs`, see also https://stackoverflow.com/a/51362214/40783 PYTHONUNBUFFERED: 1 depends_on: - mongo volumes: # admin devices use /data to store encryption package after election creation. See also directory_service.py - ../../egui_mnt/data:/egui_mnt/data # egui_mnt/export is where admin devices export election records and encryption packages - ../../egui_mnt/export/:/egui_mnt/export guardian1: image: egui:latest container_name: "guardian1" restart: always ports: - 12802:12800 environment: EG_DB_PASSWORD: ${EG_DB_PASSWORD} EG_DB_HOST: mongo EG_PORT: 12800 EG_MODE: none EG_HOST: 0.0.0.0 EG_IS_ADMIN: False PYTHONUNBUFFERED: 1 depends_on: - mongo volumes: # guardian devices store their private keys in /data so it is persists if the docker image is deleted - ../../egui_mnt/data:/egui_mnt/data guardian2: image: egui:latest container_name: "guardian2" restart: always ports: - 12803:12800 environment: EG_DB_PASSWORD: ${EG_DB_PASSWORD} EG_DB_HOST: mongo EG_PORT: 12800 EG_MODE: none EG_HOST: 0.0.0.0 EG_IS_ADMIN: False PYTHONUNBUFFERED: 1 depends_on: - mongo volumes: # guardian devices store their private keys in /data so it is persists if the docker image is deleted - ../../egui_mnt/data:/egui_mnt/data ================================================ FILE: src/electionguard_gui/eel_utils.py ================================================ from datetime import timezone, datetime import os from typing import Any, Optional def eel_fail(message: str) -> dict[str, Any]: return {"success": False, "message": message} def eel_success(result: Any = None) -> dict[str, Any]: return {"success": True, "result": result} def utc_to_str(utc_dt: Optional[datetime]) -> str: if not utc_dt: return "" local = convert_utc_to_local(utc_dt) if os.name == "nt": return local.strftime("%b %#d, %Y %#I:%M %p") return local.strftime("%b %-d, %Y %-I:%M %p") def convert_utc_to_local(utc_dt: datetime) -> datetime: return utc_dt.replace(tzinfo=timezone.utc).astimezone(None) ================================================ FILE: src/electionguard_gui/main_app.py ================================================ import traceback from typing import List import eel from electionguard_gui.components import ( ViewElectionComponent, ComponentBase, CreateElectionComponent, CreateKeyCeremonyComponent, GuardianHomeComponent, KeyCeremonyDetailsComponent, ElectionListComponent, ExportEncryptionPackageComponent, UploadBallotsComponent, CreateDecryptionComponent, ViewDecryptionComponent, ExportElectionRecordComponent, ViewTallyComponent, ViewSpoiledBallotComponent, ) from electionguard_gui.services import ( AuthorizationService, DbService, EelLogService, ServiceBase, ConfigurationService, VersionService, ) class MainApp: """Responsible for functionality related to the main app""" log_service: EelLogService db_service: DbService components: List[ComponentBase] services: List[ServiceBase] def __init__( self, log_service: EelLogService, config_service: ConfigurationService, db_service: DbService, guardian_home_component: GuardianHomeComponent, create_key_ceremony_component: CreateKeyCeremonyComponent, key_ceremony_details_component: KeyCeremonyDetailsComponent, authorization_service: AuthorizationService, create_election_component: CreateElectionComponent, view_election_component: ViewElectionComponent, election_list_component: ElectionListComponent, export_encryption_package: ExportEncryptionPackageComponent, upload_ballots_component: UploadBallotsComponent, create_decryption_component: CreateDecryptionComponent, view_decryption_component: ViewDecryptionComponent, export_election_record_component: ExportElectionRecordComponent, view_tally_component: ViewTallyComponent, view_spoiled_ballot_component: ViewSpoiledBallotComponent, version_service: VersionService, ) -> None: super().__init__() self.log_service = log_service self.db_service = db_service self.config_service = config_service self.components = [ guardian_home_component, create_key_ceremony_component, key_ceremony_details_component, create_election_component, view_election_component, election_list_component, export_encryption_package, upload_ballots_component, create_decryption_component, view_decryption_component, export_election_record_component, view_tally_component, view_spoiled_ballot_component, ] # services that need to expose methods to the UI self.services = [ authorization_service, db_service, log_service, version_service, ] def start(self) -> None: try: self.log_service.debug("Starting main app") for service in self.services: service.init() for component in self.components: component.init(self.db_service, self.log_service) self.db_service.verify_db_connection() eel.init("src/electionguard_gui/web") mode = self.config_service.get_mode() port = self.config_service.get_port() host = self.config_service.get_host() self.log_service.debug(f"Starting eel port={port} mode={mode} host={host}") eel.start( "index.html", size=(1024, 768), port=port, mode=mode, host=host, close_callback=self.on_close, ) self.log_service.info("Exiting main app normally") except Exception as e: self.log_service.error("error in main app start", e) traceback.print_exc() raise e def on_close(self, _page: str, _open_sockets: list) -> None: self.log_service.info( "To close the egui app ensure the browser tab is closed and hit Ctrl+C" ) ================================================ FILE: src/electionguard_gui/models/__init__.py ================================================ from electionguard_gui.models import decryption_dto from electionguard_gui.models import election_dto from electionguard_gui.models import key_ceremony_dto from electionguard_gui.models import key_ceremony_states from electionguard_gui.models.decryption_dto import ( DecryptionDto, GuardianDecryptionShare, ) from electionguard_gui.models.election_dto import ( ElectionDto, ) from electionguard_gui.models.key_ceremony_dto import ( KeyCeremonyDto, ) from electionguard_gui.models.key_ceremony_states import ( KeyCeremonyStates, ) __all__ = [ "DecryptionDto", "ElectionDto", "GuardianDecryptionShare", "KeyCeremonyDto", "KeyCeremonyStates", "decryption_dto", "election_dto", "key_ceremony_dto", "key_ceremony_states", ] ================================================ FILE: src/electionguard_gui/models/decryption_dto.py ================================================ from typing import Any, Dict, Optional from datetime import datetime from electionguard.decryption_share import DecryptionShare from electionguard.election_polynomial import LagrangeCoefficientsRecord from electionguard.key_ceremony import ElectionPublicKey from electionguard.serialize import from_raw from electionguard.tally import PlaintextTally, PublishedCiphertextTally from electionguard.type import BallotId from electionguard_gui.eel_utils import utc_to_str from electionguard_gui.services.authorization_service import AuthorizationService class GuardianDecryptionShare: """A guardian's contribution to a section of a tally and the spoiled ballots""" def __init__( self, guardian_id: str, decryption_share_json: str, ballot_shares: dict[str, str], guardian_key_json: str, ): self.guardian_id = guardian_id self.guardian_key = from_raw(ElectionPublicKey, guardian_key_json) self.tally_share = from_raw(DecryptionShare, decryption_share_json) self.ballot_shares = { ballot_id: from_raw(DecryptionShare, ballot_share) for (ballot_id, ballot_share) in ballot_shares.items() } guardian_id: str tally_share: DecryptionShare ballot_shares: Dict[BallotId, Optional[DecryptionShare]] guardian_key: ElectionPublicKey # pylint: disable=too-many-instance-attributes class DecryptionDto: """Responsible for serializing to the front-end GUI and providing helper functions to Python.""" decryption_id: str election_id: str election_name: Optional[str] ballot_upload_count: int ballot_count: int guardians: int quorum: int decryption_name: Optional[str] key_ceremony_id: Optional[str] guardians_joined: list[str] can_join: Optional[bool] decryption_shares: list[Any] plaintext_tally: Optional[str] plaintext_spoiled_ballots: dict[str, str] lagrange_coefficients: Optional[str] ciphertext_tally: Optional[str] completed_at_utc: Optional[datetime] completed_at_str: str created_by: Optional[str] created_at_utc: Optional[datetime] created_at_str: str def __init__(self, decryption: dict[str, Any]): self.decryption_id = str(decryption.get("_id")) self.election_id = str(decryption.get("election_id")) self.key_ceremony_id = decryption.get("key_ceremony_id") self.election_name = decryption.get("election_name") self.ballot_upload_count = _get_int(decryption, "ballot_upload_count", 0) self.ballot_count = _get_int(decryption, "ballot_count", 0) self.guardians = _get_int(decryption, "guardians", 0) self.quorum = _get_int(decryption, "quorum", 0) self.decryption_name = decryption.get("decryption_name") self.guardians_joined = _get_list(decryption, "guardians_joined") self.decryption_shares = _get_list(decryption, "decryption_shares") self.plaintext_tally = decryption.get("plaintext_tally") self.plaintext_spoiled_ballots = _get_dict( decryption, "plaintext_spoiled_ballots" ) self.lagrange_coefficients = decryption.get("lagrange_coefficients") self.ciphertext_tally = decryption.get("ciphertext_tally") self.completed_at_utc = decryption.get("completed_at") self.completed_at_str = utc_to_str(decryption.get("completed_at")) self.created_by = decryption.get("created_by") self.created_at_utc = decryption.get("created_at") self.created_at_str = utc_to_str(decryption.get("created_at")) self.can_join = False def get_status(self) -> str: if len(self.guardians_joined) < self.guardians: return "waiting for all guardians to join" if self.completed_at_utc is None: return "performing decryption" return "decryption complete" def to_id_name_dict(self) -> dict[str, Any]: return { "id": self.decryption_id, "decryption_name": self.decryption_name, } def to_dict(self) -> dict[str, Any]: return { "decryption_id": self.decryption_id, "election_id": self.election_id, "election_name": self.election_name, "ballot_upload_count": self.ballot_upload_count, "ballot_count": self.ballot_count, "decryption_name": self.decryption_name, "guardians_joined": self.guardians_joined, "status": self.get_status(), "completed_at_str": self.completed_at_str, "spoiled_ballots": ( list(self.plaintext_spoiled_ballots.keys()) if self.plaintext_spoiled_ballots else [] ), "can_join": self.can_join, "created_by": self.created_by, "created_at": self.created_at_str, } def get_decryption_shares(self) -> list[GuardianDecryptionShare]: return [ GuardianDecryptionShare( ballot_share_dict["guardian_id"], ballot_share_dict["decryption_share"], ballot_share_dict["ballot_shares"], ballot_share_dict["guardian_key"], ) for ballot_share_dict in self.decryption_shares ] def set_can_join(self, auth_service: AuthorizationService) -> None: user_id = auth_service.get_user_id() already_joined = user_id in self.guardians_joined is_admin = auth_service.is_admin() self.can_join = not already_joined and not is_admin def get_plaintext_tally(self) -> PlaintextTally: if not self.plaintext_tally: raise ValueError("No plaintext tally found") return from_raw(PlaintextTally, self.plaintext_tally) def get_plaintext_spoiled_ballots(self) -> list[PlaintextTally]: return [ from_raw(PlaintextTally, tally) for tally in self.plaintext_spoiled_ballots.values() ] def get_lagrange_coefficients(self) -> LagrangeCoefficientsRecord: if not self.lagrange_coefficients: raise ValueError("No lagrange coefficients found") return from_raw(LagrangeCoefficientsRecord, self.lagrange_coefficients) def get_ciphertext_tally(self) -> PublishedCiphertextTally: if not self.ciphertext_tally: raise ValueError("No ciphertext tally found") return from_raw(PublishedCiphertextTally, self.ciphertext_tally) def _get_list(decryption: dict[str, Any], name: str) -> list: value = decryption.get(name) if value: return list(value) return [] def _get_dict(decryption: dict[str, Any], name: str) -> dict: value = decryption.get(name) if value: return dict(value) return {} def _get_int(decryption: dict[str, Any], name: str, default: int) -> int: value = decryption.get(name) if value: return int(value) return default ================================================ FILE: src/electionguard_gui/models/election_dto.py ================================================ from typing import Any, Optional from datetime import datetime from electionguard.election import CiphertextElectionContext from electionguard.encrypt import EncryptionDevice from electionguard.guardian import GuardianRecord from electionguard.manifest import Manifest from electionguard.serialize import from_list_raw, from_raw from electionguard_gui.eel_utils import utc_to_str # pylint: disable=too-many-instance-attributes class ElectionDto: """Responsible for serializing to the front-end GUI and providing helper functions to Python.""" id: str election_name: Optional[str] key_ceremony_id: Optional[str] guardians: Optional[int] quorum: Optional[int] manifest: Optional[dict[str, Any]] context: Optional[str] constants: Optional[int] guardian_records: Optional[str] encryption_package_file: Optional[str] election_url: Optional[str] ballot_uploads: list[dict[str, Any]] decryptions: list[dict[str, Any]] created_by: Optional[str] created_at_utc: Optional[datetime] created_at_str: str def __init__(self, election: dict[str, Any]): self.id = str(election.get("_id")) self.election_name = election.get("election_name") self.key_ceremony_id = election.get("key_ceremony_id") self.guardians = election.get("guardians") self.quorum = election.get("quorum") self.manifest = election.get("manifest") self.context = election.get("context") self.constants = election.get("constants") self.guardian_records = election.get("guardian_records") self.encryption_package_file = election.get("encryption_package_file") self.election_url = election.get("election_url") self.ballot_uploads = _get_list(election, "ballot_uploads") self.decryptions = _get_list(election, "decryptions") self.created_by = election.get("created_by") self.created_at_utc = election.get("created_at") self.created_at_str = utc_to_str(election.get("created_at")) def to_id_name_dict(self) -> dict[str, Any]: return { "id": self.id, "election_name": self.election_name, } def _get_manifest_field(self, field: str) -> Any: return self.manifest.get(field) if self.manifest else None def to_dict(self) -> dict[str, Any]: return { "id": self.id, "election_name": self.election_name, "guardians": self.guardians, "quorum": self.quorum, "election_url": self.election_url, "manifest": { "name": self._get_manifest_field("name"), "scope": self._get_manifest_field("scope"), "geopolitical_units": self._get_manifest_field("geopolitical_units"), "parties": self._get_manifest_field("parties"), "candidates": self._get_manifest_field("candidates"), "contests": self._get_manifest_field("contests"), "ballot_styles": self._get_manifest_field("ballot_styles"), }, "ballot_uploads": [ { "location": ballot_upload["location"], "ballot_count": ballot_upload["ballot_count"], "created_at": utc_to_str(ballot_upload.get("created_at")), } for ballot_upload in self.ballot_uploads ], "decryptions": [ { "decryption_id": decryption["decryption_id"], "name": decryption["name"], "created_at": utc_to_str(decryption.get("created_at")), } for decryption in self.decryptions ], "created_by": self.created_by, "created_at": self.created_at_str, } def get_manifest(self) -> Manifest: if not self.manifest: raise Exception("No manifest found") return from_raw(Manifest, self.manifest["raw"]) def get_context(self) -> CiphertextElectionContext: if not self.context: raise Exception("No context found") return from_raw(CiphertextElectionContext, self.context) def get_encryption_devices(self) -> list[EncryptionDevice]: return [ EncryptionDevice( ballot_upload["device_id"], ballot_upload["session_id"], ballot_upload["launch_code"], ballot_upload["location"], ) for ballot_upload in self.ballot_uploads ] def get_guardian_records(self) -> list[GuardianRecord]: if not self.guardian_records: raise Exception("No guardian records found") return from_list_raw(GuardianRecord, self.guardian_records) def get_guardian_sequence_order(self, guardian_id: str) -> int: for record in self.get_guardian_records(): if record.guardian_id == guardian_id: return record.sequence_order raise Exception("Guardian not found") def sum_ballots(self) -> int: return sum(ballot["ballot_count"] for ballot in self.ballot_uploads) def _get_list(election: dict[str, Any], name: str) -> list: value = election.get(name) if value: return list(value) return [] ================================================ FILE: src/electionguard_gui/models/key_ceremony_dto.py ================================================ from typing import Any, List from datetime import datetime from electionguard import ElectionPartialKeyVerification from electionguard.group import ElementModP, ElementModQ from electionguard.key_ceremony import ( ElectionJointKey, ElectionPartialKeyBackup, ElectionPublicKey, ) from electionguard.election_polynomial import PublicCommitment from electionguard.elgamal import ElGamalPublicKey, HashedElGamalCiphertext from electionguard.schnorr import SchnorrProof from electionguard_gui.eel_utils import utc_to_str from electionguard_gui.services.authorization_service import AuthorizationService # pylint: disable=too-many-instance-attributes class KeyCeremonyDto: """A key ceremony for serializing to the front-end GUI and providing helper functions to Python.""" def __init__(self, key_ceremony: Any): self.id = str(key_ceremony["_id"]) self.guardian_count = key_ceremony["guardian_count"] self.key_ceremony_name = key_ceremony["key_ceremony_name"] self.quorum = key_ceremony["quorum"] self.guardians_joined = key_ceremony["guardians_joined"] self.other_keys = key_ceremony["other_keys"] self.backups = key_ceremony["backups"] self.shared_backups = key_ceremony["shared_backups"] self.verifications = key_ceremony["verifications"] self.keys = [_dict_to_election_public_key(key) for key in key_ceremony["keys"]] self.joint_key = key_ceremony["joint_key"] self.created_by = key_ceremony["created_by"] self.created_at_utc = key_ceremony["created_at"] self.created_at_str = utc_to_str(key_ceremony["created_at"]) self.completed_at_utc = key_ceremony["completed_at"] self.completed_at_str = utc_to_str(key_ceremony["completed_at"]) def to_id_name_dict(self) -> dict[str, Any]: return { "id": self.id, "key_ceremony_name": self.key_ceremony_name, } def to_dict(self) -> dict[str, Any]: return { "id": self.id, "guardian_count": self.guardian_count, "key_ceremony_name": self.key_ceremony_name, "quorum": self.quorum, "guardians_joined": self.guardians_joined, "created_by": self.created_by, "created_at_str": self.created_at_str, "completed_at_str": self.completed_at_str, "can_join": self.can_join, "status": self.status, } id: str guardian_count: int key_ceremony_name: str quorum: int guardians_joined: List[str] other_keys: List[Any] backups: List[Any] shared_backups: List[Any] verifications: List[Any] keys: List[ElectionPublicKey] joint_key: Any created_by: str created_at_utc: datetime completed_at_utc: datetime created_at_str: str completed_at_str: str can_join: bool status: str def find_key(self, guardian_id: str) -> ElectionPublicKey: keys = self.keys key = next((key for key in keys if key.owner_id == guardian_id), None) if key is None: raise Exception("Key not found for guardian: " + guardian_id) return key def get_backup_count_for_user(self, user_id: str) -> int: backups = [backup for backup in self.backups if backup["owner_id"] == user_id] return len(backups) def get_verification_count_for_user(self, user_id: str) -> int: return len( [ verification for verification in self.verifications if verification["designated_id"] == user_id ] ) def get_verifications(self) -> List[ElectionPartialKeyVerification]: return [ _dict_to_verification(verification) for verification in self.verifications ] def get_shared_backups_for_guardian( self, guardian_id: str ) -> List[ElectionPartialKeyBackup]: shared_backup_wrapper = next( filter( lambda backup: backup["owner_id"] == guardian_id, self.shared_backups ) ) backups = shared_backup_wrapper["backups"] return [_dict_to_backup(backup) for backup in backups] def get_backups(self) -> List[ElectionPartialKeyBackup]: return [_dict_to_backup(backup) for backup in self.backups] def find_other_keys_for_user(self, user_id: str) -> List[ElectionPublicKey]: other_key_wrapper = next( filter( lambda other_key: other_key["owner_id"] == user_id, self.other_keys, ) ) other_keys = other_key_wrapper["other_keys"] return [_dict_to_election_public_key(other_key) for other_key in other_keys] def joint_key_exists(self) -> bool: return self.joint_key is not None def get_joint_key(self) -> ElectionJointKey: return ElectionJointKey( ElGamalPublicKey(self.joint_key["joint_public_key"]), ElementModQ(self.joint_key["commitment_hash"]), ) def set_can_join(self, auth_service: AuthorizationService) -> None: user_id = auth_service.get_user_id() already_joined = user_id in self.guardians_joined is_admin = auth_service.is_admin() self.can_join = not already_joined and not is_admin def _dict_to_verification(verification: Any) -> ElectionPartialKeyVerification: return ElectionPartialKeyVerification( verification["owner_id"], verification["designated_id"], verification["verifier_id"], verification["verified"], ) def _dict_to_backup(backup: Any) -> ElectionPartialKeyBackup: coordinate = backup["encrypted_coordinate"] ciphertext = HashedElGamalCiphertext( ElementModP(coordinate["pad"]), coordinate["data"], coordinate["mac"] ) return ElectionPartialKeyBackup( backup["owner_id"], backup["designated_id"], backup["designated_sequence_order"], ciphertext, ) def _dict_to_election_public_key(key: Any) -> ElectionPublicKey: coefficient_commitments = [ PublicCommitment(x) for x in key["coefficient_commitments"] ] coefficient_proofs = [ SchnorrProof( cp["public_key"], cp["commitment"], cp["challenge"], cp["response"], cp["usage"], ) for cp in key["coefficient_proofs"] ] guardian_public_key = ElectionPublicKey( key["owner_id"], key["sequence_order"], ElGamalPublicKey(key["key"]), coefficient_commitments, coefficient_proofs, ) return guardian_public_key ================================================ FILE: src/electionguard_gui/models/key_ceremony_states.py ================================================ from enum import Enum class KeyCeremonyStates(Enum): """A list of states for the key ceremony.""" PendingGuardiansJoin = 1 PendingAdminAnnounce = 2 PendingGuardianBackups = 3 PendingAdminToShareBackups = 4 PendingGuardiansVerifyBackups = 5 PendingAdminToPublishJointKey = 6 Complete = 7 ================================================ FILE: src/electionguard_gui/services/__init__.py ================================================ from electionguard_gui.services import authorization_service from electionguard_gui.services import ballot_upload_service from electionguard_gui.services import configuration_service from electionguard_gui.services import db_serialization_service from electionguard_gui.services import db_service from electionguard_gui.services import db_watcher_service from electionguard_gui.services import decryption_service from electionguard_gui.services import decryption_stages from electionguard_gui.services import directory_service from electionguard_gui.services import eel_log_service from electionguard_gui.services import election_service from electionguard_gui.services import export_service from electionguard_gui.services import guardian_service from electionguard_gui.services import gui_setup_input_retrieval_step from electionguard_gui.services import key_ceremony_service from electionguard_gui.services import key_ceremony_stages from electionguard_gui.services import key_ceremony_state_service from electionguard_gui.services import plaintext_ballot_service from electionguard_gui.services import service_base from electionguard_gui.services import version_service from electionguard_gui.services.authorization_service import ( AuthorizationService, ) from electionguard_gui.services.ballot_upload_service import ( BallotUploadService, RetryException, ) from electionguard_gui.services.configuration_service import ( ConfigurationService, DB_HOST_KEY, DB_PASSWORD_KEY, HOST_KEY, IS_ADMIN_KEY, MODE_KEY, PORT_KEY, ) from electionguard_gui.services.db_serialization_service import ( backup_to_dict, joint_key_to_dict, public_key_to_dict, verification_to_dict, ) from electionguard_gui.services.db_service import ( DbService, ) from electionguard_gui.services.db_watcher_service import ( DbWatcherService, ) from electionguard_gui.services.decryption_service import ( DecryptionService, to_ballot_share_raw, ) from electionguard_gui.services.decryption_stages import ( DecryptionS1JoinService, DecryptionS2AnnounceService, DecryptionStageBase, decryption_s1_join_service, decryption_s2_announce_service, decryption_stage_base, get_tally, ) from electionguard_gui.services.directory_service import ( DOCKER_MOUNT_DIR, get_data_dir, get_export_dir, ) from electionguard_gui.services.eel_log_service import ( EelLogService, ) from electionguard_gui.services.election_service import ( ElectionService, ) from electionguard_gui.services.export_service import ( get_export_locations, get_removable_drives, ) from electionguard_gui.services.guardian_service import ( GuardianService, announce_guardians, make_guardian, make_mediator, ) from electionguard_gui.services.gui_setup_input_retrieval_step import ( GuiSetupInputRetrievalStep, ) from electionguard_gui.services.key_ceremony_service import ( KeyCeremonyService, get_guardian_number, ) from electionguard_gui.services.key_ceremony_stages import ( KeyCeremonyS1JoinService, KeyCeremonyS2AnnounceService, KeyCeremonyS3MakeBackupService, KeyCeremonyS4ShareBackupService, KeyCeremonyS5VerifyBackupService, KeyCeremonyS6PublishKeyService, KeyCeremonyStageBase, key_ceremony_s1_join_service, key_ceremony_s2_announce_service, key_ceremony_s3_make_backup_service, key_ceremony_s4_share_backup_service, key_ceremony_s5_verify_backup_service, key_ceremony_s6_publish_key_service, key_ceremony_stage_base, ) from electionguard_gui.services.key_ceremony_state_service import ( KeyCeremonyStateService, get_key_ceremony_status, status_descriptions, ) from electionguard_gui.services.plaintext_ballot_service import ( get_plaintext_ballot_report, ) from electionguard_gui.services.service_base import ( ServiceBase, ) from electionguard_gui.services.version_service import ( VersionService, ) __all__ = [ "AuthorizationService", "BallotUploadService", "ConfigurationService", "DB_HOST_KEY", "DB_PASSWORD_KEY", "DOCKER_MOUNT_DIR", "DbService", "DbWatcherService", "DecryptionS1JoinService", "DecryptionS2AnnounceService", "DecryptionService", "DecryptionStageBase", "EelLogService", "ElectionService", "GuardianService", "GuiSetupInputRetrievalStep", "HOST_KEY", "IS_ADMIN_KEY", "KeyCeremonyS1JoinService", "KeyCeremonyS2AnnounceService", "KeyCeremonyS3MakeBackupService", "KeyCeremonyS4ShareBackupService", "KeyCeremonyS5VerifyBackupService", "KeyCeremonyS6PublishKeyService", "KeyCeremonyService", "KeyCeremonyStageBase", "KeyCeremonyStateService", "MODE_KEY", "PORT_KEY", "RetryException", "ServiceBase", "VersionService", "announce_guardians", "authorization_service", "backup_to_dict", "ballot_upload_service", "configuration_service", "db_serialization_service", "db_service", "db_watcher_service", "decryption_s1_join_service", "decryption_s2_announce_service", "decryption_service", "decryption_stage_base", "decryption_stages", "directory_service", "eel_log_service", "election_service", "export_service", "get_data_dir", "get_export_dir", "get_export_locations", "get_guardian_number", "get_key_ceremony_status", "get_plaintext_ballot_report", "get_removable_drives", "get_tally", "guardian_service", "gui_setup_input_retrieval_step", "joint_key_to_dict", "key_ceremony_s1_join_service", "key_ceremony_s2_announce_service", "key_ceremony_s3_make_backup_service", "key_ceremony_s4_share_backup_service", "key_ceremony_s5_verify_backup_service", "key_ceremony_s6_publish_key_service", "key_ceremony_service", "key_ceremony_stage_base", "key_ceremony_stages", "key_ceremony_state_service", "make_guardian", "make_mediator", "plaintext_ballot_service", "public_key_to_dict", "service_base", "status_descriptions", "to_ballot_share_raw", "verification_to_dict", "version_service", ] ================================================ FILE: src/electionguard_gui/services/authorization_service.py ================================================ from typing import Optional import eel from electionguard_gui.services.configuration_service import ConfigurationService from electionguard_gui.services.service_base import ServiceBase class AuthorizationService(ServiceBase): """Responsible for functionality related to authorization and user identify""" _is_admin: bool def __init__(self, config_service: ConfigurationService) -> None: self._is_admin = config_service.get_is_admin() # todo: replace state based storage with configparser https://docs.python.org/3/library/configparser.html user_id: Optional[str] = None def expose(self) -> None: eel.expose(self.get_user_id) eel.expose(self.set_user_id) eel.expose(self.is_admin) def get_required_user_id(self) -> str: if self.user_id is None: raise Exception("User must be logged in") return self.user_id def get_user_id(self) -> Optional[str]: return self.user_id def set_user_id(self, user_id: str) -> None: self.user_id = user_id def is_admin(self) -> bool: return self._is_admin ================================================ FILE: src/electionguard_gui/services/ballot_upload_service.py ================================================ from datetime import datetime from time import sleep from typing import Callable from pymongo.database import Database from electionguard.ballot import SubmittedBallot from electionguard.serialize import from_raw from electionguard_gui.services.eel_log_service import EelLogService from electionguard_gui.services.service_base import ServiceBase from electionguard_gui.services.authorization_service import AuthorizationService class BallotUploadService(ServiceBase): """Responsible for functionality related to ballot uploads""" _log: EelLogService _auth_service: AuthorizationService def __init__( self, log_service: EelLogService, auth_service: AuthorizationService ) -> None: self._log = log_service self._auth_service = auth_service def create( self, db: Database, election_id: str, device_file_name: str, device_file_contents: str, created_at: datetime, ) -> str: ballot_upload = { "election_id": election_id, "device_file_name": device_file_name, "device_file_contents": device_file_contents, "ballot_count": 0, "created_by": self._auth_service.get_user_id(), "created_at": created_at, } self._log.trace(f"inserting ballot upload for: {election_id}") inserted_id = db.ballot_uploads.insert_one(ballot_upload).inserted_id return str(inserted_id) def add_ballot( self, db: Database, ballot_upload_id: str, election_id: str, file_name: str, file_contents: str, ballot_object_id: str, ) -> bool: self._log.trace(f"adding ballot {file_name} to {ballot_upload_id}") db.ballot_uploads.insert_one( { "ballot_upload_id": ballot_upload_id, "election_id": election_id, "file_name": file_name, "object_id": ballot_object_id, "file_contents": file_contents, } ) return True def increment_ballot_count(self, db: Database, ballot_upload_id: str) -> None: self._log.trace(f"incrementing ballot count for {ballot_upload_id}") db.ballot_uploads.update_one( {"_id": ballot_upload_id, "ballot_count": {"$exists": True}}, {"$inc": {"ballot_count": 1}}, ) def any_ballot_exists(self, db: Database, election_id: str, object_id: str) -> bool: self._log.trace("checking if ballot exists for {election_id}") return ( db.ballot_uploads.count_documents( {"election_id": election_id, "object_id": object_id} ) > 0 ) def get_ballots( self, db: Database, election_id: str, report_status: Callable[[str], None] ) -> list[SubmittedBallot]: self._log.debug(f"getting ballots for {election_id}") ballot_uploads = list( db.ballot_uploads.find( {"election_id": election_id, "file_contents": {"$exists": True}}, projection={"_id": 1, "file_contents": 0}, ) ) ballots = [] total_ballots = len(ballot_uploads) ballot_num = 1 for ballot_id_obj in ballot_uploads: ballot_id = ballot_id_obj["_id"] report_status(f"Loading ballot {ballot_num}/{total_ballots}") try: ballot = self.get_submitted_ballot_with_retry(db, ballot_id) ballots.append(ballot) # pylint: disable=broad-except except Exception as e: self._log.error( f"Error deserializing ballot {ballot_id}. " + "Skipping ballot, but this may cause Chaum Pederson errors later.", e, ) # per RC 8/15/22 log errors and continue processing even if it makes numbers incorrect ballot_num += 1 return ballots def get_submitted_ballot_with_retry( self, db: Database, ballot_upload_id: str ) -> SubmittedBallot: retry_num = 0 max_retries = 3 while retry_num < max_retries: try: return self.get_submitted_ballot(db, ballot_upload_id) except RetryException: self._log.warn( f"retrying get ballot {ballot_upload_id} in {retry_num + 1} second(s). Retry #{retry_num + 1}" ) # wait 1 second before retrying in case network was slow sleep(retry_num + 1) retry_num += 1 raise Exception( f"Failed to get ballot {ballot_upload_id} after {max_retries} retries" ) def get_submitted_ballot( self, db: Database, ballot_upload_id: str ) -> SubmittedBallot: self._log.trace(f"getting submitted ballot {ballot_upload_id}") ballot_obj = None try: ballot_obj = db.ballot_uploads.find_one( {"_id": ballot_upload_id}, projection={"file_contents": 1} ) except Exception as e: self._log.error(f"mongo error getting ballot {ballot_upload_id}", e) raise RetryException from e if ballot_obj is None: raise Exception(f"Ballot {ballot_upload_id} not found") ballot_str = ballot_obj["file_contents"] # if ballot_str ends with a } if not ballot_str[-1] == "}": self._log.warn(f"ballot {ballot_upload_id} is missing a closing bracket") raise RetryException try: ballot = from_raw(SubmittedBallot, ballot_str) except Exception as e: self._log.error(f"error deserializing ballot {ballot_upload_id}", e) raise RetryException from e return ballot class RetryException(Exception): """An exception to notify the caller to retry""" ================================================ FILE: src/electionguard_gui/services/configuration_service.py ================================================ from os import environ from sys import exit from typing import Optional DB_PASSWORD_KEY = "EG_DB_PASSWORD" DB_HOST_KEY = "EG_DB_HOST" IS_ADMIN_KEY = "EG_IS_ADMIN" PORT_KEY = "EG_PORT" MODE_KEY = "EG_MODE" HOST_KEY = "EG_HOST" class ConfigurationService: """Responsible for retrieving configuration values, generally from environment variables""" # 'chrome', 'electron', 'edge', 'custom', or 'none', see also https://github.com/ChrisKnott/Eel#app-options def get_mode(self) -> Optional[str]: mode = self._get_param_or_default(MODE_KEY, "chrome") return None if mode == "none" else mode def get_port(self) -> int: return int(self._get_param_or_default(PORT_KEY, "0")) def get_host(self) -> str: return str(self._get_param_or_default(HOST_KEY, "localhost")) def get_db_password(self) -> str: return self._get_param(DB_PASSWORD_KEY) def get_db_host(self, default: str) -> str: return self._get_param_or_default(DB_HOST_KEY, default) def get_is_admin(self) -> bool: return self._get_param_or_default(IS_ADMIN_KEY, "false").lower() == "true" def _get_param(self, param_name: str) -> str: try: return environ[param_name] except KeyError: print(f"The environment variable {param_name} is not set.") exit(1) def _get_param_or_default(self, param_name: str, default: str) -> str: try: return environ[param_name] except KeyError: return default ================================================ FILE: src/electionguard_gui/services/db_serialization_service.py ================================================ from typing import Any from electionguard.key_ceremony import ( ElectionJointKey, ElectionPartialKeyBackup, ElectionPartialKeyVerification, ElectionPublicKey, ) def public_key_to_dict(key: ElectionPublicKey) -> dict[str, Any]: return { "owner_id": key.owner_id, "sequence_order": key.sequence_order, "key": str(key.key), "coefficient_commitments": [str(c) for c in key.coefficient_commitments], "coefficient_proofs": [ { "public_key": str(cp.public_key), "commitment": str(cp.commitment), "challenge": str(cp.challenge), "response": str(cp.response), "usage": str(cp.usage), } for cp in key.coefficient_proofs ], } def backup_to_dict(backup: ElectionPartialKeyBackup) -> dict[str, Any]: coordinate = backup.encrypted_coordinate return { "owner_id": backup.owner_id, "designated_id": backup.designated_id, "designated_sequence_order": backup.designated_sequence_order, "encrypted_coordinate": { "pad": str(coordinate.pad), "data": coordinate.data, "mac": coordinate.mac, }, } def verification_to_dict( verification: ElectionPartialKeyVerification, ) -> dict[str, Any]: return { "owner_id": verification.owner_id, "designated_id": verification.designated_id, "verifier_id": verification.verifier_id, "verified": verification.verified, } def joint_key_to_dict( key: ElectionJointKey, ) -> dict[str, Any]: return { "joint_public_key": str(key.joint_public_key), "commitment_hash": str(key.commitment_hash), } ================================================ FILE: src/electionguard_gui/services/db_service.py ================================================ from pymongo import MongoClient from pymongo.database import Database from electionguard_gui.services.configuration_service import ( ConfigurationService, ) from electionguard_gui.services.eel_log_service import EelLogService from electionguard_gui.services.service_base import ServiceBase class DbService(ServiceBase): """Responsible for instantiating a database""" log_service: EelLogService def __init__( self, log_service: EelLogService, config_service: ConfigurationService ) -> None: self.log_service = log_service self._db_password = config_service.get_db_password() self._db_host = config_service.get_db_host(self.DEFAULT_HOST) DEFAULT_HOST = "localhost" DEFAULT_PORT = 27017 DEFAULT_USERNAME = "root" _db_password: str _db_host: str def get_db(self) -> Database: client: MongoClient = MongoClient( self._db_host, self.DEFAULT_PORT, username=self.DEFAULT_USERNAME, password=self._db_password, ) db: Database = client.ElectionGuardDb return db def verify_db_connection(self) -> None: self.log_service.debug("Verifying database connection") db = self.get_db() db.list_collections() ================================================ FILE: src/electionguard_gui/services/db_watcher_service.py ================================================ from typing import Callable, Optional from threading import Event import eel from pymongo.database import Database from pymongo import CursorType from electionguard_gui.services.eel_log_service import EelLogService from electionguard_gui.services.service_base import ServiceBase class DbWatcherService(ServiceBase): """ Responsible for long polling against the database in order to notify clients that changes have occurred to data within collections """ _log: EelLogService def __init__(self, log_service: EelLogService) -> None: self._log = log_service MS_TO_BLOCK = 200 # assumptions: 1. only one thread will be watching the database at a time, and 2. a class instance will be # maintained for the duration of the time watching the database. However, both will always be true given # how eel works. watching_database = Event() def notify_changed(self, db: Database, collection: str, id: str) -> None: # notify any watchers that the collection was modified self._log.debug(f"notifying watchers of change to {collection} for {id}") db.db_deltas.insert_one({"collection": collection, "changed_id": id}) def watch_database( self, db: Database, id_to_watch: Optional[str], on_found: Callable[[str, str], None], ) -> None: # retrieve a tailable cursor of the deltas in the database to avoid polling cursor = db.db_deltas.find( {}, cursor_type=CursorType.TAILABLE_AWAIT ).max_await_time_ms(self.MS_TO_BLOCK) # burn through all updates that have occurred up till now so next time we only get new ones for _ in cursor: pass if self.watching_database.is_set(): self.stop_watching() # set a semaphore to indicate that we are watching the database self.watching_database.set() while self.watching_database.is_set() and cursor.alive: try: # block for up to a few seconds until someone adds a new delta delta = cursor.next() collection = delta["collection"] changed_id = delta["changed_id"] if id_to_watch is None or id_to_watch == changed_id: self._log.debug(f"new delta found for {collection} {changed_id}") on_found(collection, changed_id) except StopIteration: # the tailable cursor times out after a few seconds and fires a StopIteration exception, # so we need to catch it and restart watching. The sleep is required by eel to allow # it to respond to events such as the very important stop_watching event. eel.sleep(0.8) def stop_watching(self) -> None: self.watching_database.clear() ================================================ FILE: src/electionguard_gui/services/decryption_service.py ================================================ from datetime import datetime, timezone from typing import Any, Dict, List, Optional from bson import ObjectId from pymongo.database import Database from electionguard.decryption_share import DecryptionShare from electionguard.election_polynomial import LagrangeCoefficientsRecord from electionguard.key_ceremony import ElectionPublicKey from electionguard.serialize import to_raw from electionguard.tally import PlaintextTally, PublishedCiphertextTally from electionguard.type import BallotId from electionguard_gui.models.decryption_dto import DecryptionDto from electionguard_gui.models.election_dto import ElectionDto from electionguard_gui.services.db_watcher_service import DbWatcherService from electionguard_gui.services.eel_log_service import EelLogService from electionguard_gui.services.service_base import ServiceBase from electionguard_gui.services.authorization_service import AuthorizationService class DecryptionService(ServiceBase): """Responsible for functionality related to decryption operations""" _log: EelLogService _auth_service: AuthorizationService _db_watcher_service: DbWatcherService def __init__( self, log_service: EelLogService, auth_service: AuthorizationService, db_watcher_service: DbWatcherService, ) -> None: self._log = log_service self._auth_service = auth_service self._db_watcher_service = db_watcher_service def create( self, db: Database, election: ElectionDto, decryption_name: str, ) -> str: ballot_count = election.sum_ballots() ballot_upload_count = len(election.ballot_uploads) decryption: dict[str, Any] = { "election_id": election.id, "election_name": election.election_name, "ballot_count": ballot_count, "ballot_upload_count": ballot_upload_count, "key_ceremony_id": election.key_ceremony_id, "guardians": election.guardians, "quorum": election.quorum, "decryption_name": decryption_name, "guardians_joined": [], "decryption_shares": [], "plaintext_spoiled_ballots": None, "plaintext_tally": None, "lagrange_coefficients": None, "ciphertext_tally": None, "completed_at": None, "created_by": self._auth_service.get_user_id(), "created_at": datetime.now(timezone.utc), } self._log.trace(f"inserting decryption for: {election.id}") insert_result = db.decryptions.insert_one(decryption) inserted_id = str(insert_result.inserted_id) self.notify_changed(db, inserted_id) return inserted_id def notify_changed(self, db: Database, decryption_id: str) -> None: self._db_watcher_service.notify_changed(db, "decryptions", decryption_id) def name_exists(self, db: Database, name: str) -> Any: self._log.trace(f"getting decryption by name: {name}") decryption = db.decryptions.find_one({"decryption_name": name}) return decryption is not None def get(self, db: Database, decryption_id: str) -> DecryptionDto: self._log.trace(f"getting decryption {decryption_id}") decryption = db.decryptions.find_one({"_id": ObjectId(decryption_id)}) if decryption is None: raise Exception(f"decryption {decryption_id} not found") dto = DecryptionDto(decryption) dto.set_can_join(self._auth_service) return dto def get_decryption_count(self, db: Database, election_id: str) -> int: self._log.trace(f"getting decryption count for election {election_id}") decryption_count: int = db.decryptions.count_documents( {"election_id": election_id} ) return decryption_count def get_active(self, db: Database) -> List[DecryptionDto]: self._log.trace("getting all decryptions") decryption_cursor = db.decryptions.find( { "completed_at": None, } ) decryption_list = [ DecryptionDto(decryption) for decryption in decryption_cursor ] return decryption_list def append_guardian_joined( self, db: Database, decryption_id: str, guardian_id: str, decryption_share: DecryptionShare, ballot_shares: Dict[BallotId, Optional[DecryptionShare]], guardian_key: ElectionPublicKey, ) -> None: decryption_share_raw = to_raw(decryption_share) self._log.trace( f"appending guardian {guardian_id} to decryption {decryption_id}" ) ballot_shares_dict = { ballot_id: to_ballot_share_raw(ballot_share) for (ballot_id, ballot_share) in ballot_shares.items() } db.decryptions.update_one( {"_id": ObjectId(decryption_id)}, { "$push": { "decryption_shares": { "guardian_id": guardian_id, "guardian_key": to_raw(guardian_key), "decryption_share": decryption_share_raw, "ballot_shares": ballot_shares_dict, } } }, ) db.decryptions.update_one( {"_id": ObjectId(decryption_id)}, {"$push": {"guardians_joined": guardian_id}}, ) def set_decryption_completed( self, db: Database, decryption_id: str, plaintext_tally: PlaintextTally, plaintext_spoiled_ballots: Dict[BallotId, PlaintextTally], lagrange_coefficients: LagrangeCoefficientsRecord, ciphertext_tally: PublishedCiphertextTally, ) -> None: self._log.trace("setting decryption completed") plaintext_spoiled_ballots_dict = { str(ballot_id): to_raw(plaintext_tally) for (ballot_id, plaintext_tally) in plaintext_spoiled_ballots.items() } db.decryptions.update_one( {"_id": ObjectId(decryption_id)}, { "$set": { "completed_at": datetime.now(timezone.utc), "plaintext_tally": to_raw(plaintext_tally), "plaintext_spoiled_ballots": plaintext_spoiled_ballots_dict, "lagrange_coefficients": to_raw(lagrange_coefficients), "ciphertext_tally": to_raw(ciphertext_tally), } }, ) def to_ballot_share_raw(ballot_share: Optional[DecryptionShare]) -> Optional[str]: if ballot_share is None: return None return to_raw(ballot_share) ================================================ FILE: src/electionguard_gui/services/decryption_stages/__init__.py ================================================ from electionguard_gui.services.decryption_stages import decryption_s1_join_service from electionguard_gui.services.decryption_stages import decryption_s2_announce_service from electionguard_gui.services.decryption_stages import decryption_stage_base from electionguard_gui.services.decryption_stages.decryption_s1_join_service import ( DecryptionS1JoinService, ) from electionguard_gui.services.decryption_stages.decryption_s2_announce_service import ( DecryptionS2AnnounceService, ) from electionguard_gui.services.decryption_stages.decryption_stage_base import ( DecryptionStageBase, get_tally, ) __all__ = [ "DecryptionS1JoinService", "DecryptionS2AnnounceService", "DecryptionStageBase", "decryption_s1_join_service", "decryption_s2_announce_service", "decryption_stage_base", "get_tally", ] ================================================ FILE: src/electionguard_gui/services/decryption_stages/decryption_s1_join_service.py ================================================ from pymongo.database import Database import eel # type: ignore[import-untyped] from electionguard.ballot import BallotBoxState from electionguard_gui.models.decryption_dto import DecryptionDto from electionguard_gui.services.decryption_stages.decryption_stage_base import ( DecryptionStageBase, get_tally, ) class DecryptionS1JoinService(DecryptionStageBase): """Responsible for the 1st stage during a decryption were guardians join the decryption""" def run(self, db: Database, decryption: DecryptionDto) -> None: _update_decrypt_status("Starting tally") current_user_id = self._auth_service.get_required_user_id() self._log.info(f"S1: {current_user_id} decrypting {decryption.decryption_id}") election = self._election_service.get(db, decryption.election_id) manifest = election.get_manifest() context = election.get_context() guardian = self._guardian_service.load_guardian_from_decryption( current_user_id, decryption ) ballots = self._ballot_upload_service.get_ballots( db, election.id, _update_decrypt_status ) _update_decrypt_status("Calculating tally") self._log.debug(f"getting tally for {len(ballots)} ballots") ciphertext_tally = get_tally(manifest, context, ballots, False) self._log.debug("computing tally share") decryption_share = guardian.compute_tally_share(ciphertext_tally, context) if decryption_share is None: raise Exception("No decryption_shares found") _update_decrypt_status("Calculating spoiled ballots") self._log.debug("decrypting spoiled ballots") spoiled_ballots = [ ballot for ballot in ballots if ballot.state == BallotBoxState.SPOILED ] ballot_shares = guardian.compute_ballot_shares(spoiled_ballots, context) if ballot_shares is None: raise Exception("No ballot shares found") guardian_key = guardian.share_key() _update_decrypt_status("Finalizing tally") self._decryption_service.append_guardian_joined( db, decryption.decryption_id, current_user_id, decryption_share, ballot_shares, guardian_key, ) self._log.debug("Completed tally") self._decryption_service.notify_changed(db, decryption.decryption_id) def _update_decrypt_status(status: str) -> None: # pylint: disable=no-member eel.update_decrypt_status(status) # type: ignore[attr-defined] ================================================ FILE: src/electionguard_gui/services/decryption_stages/decryption_s2_announce_service.py ================================================ from pymongo.database import Database import eel # type: ignore[import-untyped] from electionguard import DecryptionMediator from electionguard.ballot import BallotBoxState from electionguard.election_polynomial import LagrangeCoefficientsRecord from electionguard_gui.models.decryption_dto import DecryptionDto from electionguard_gui.services.decryption_stages.decryption_stage_base import ( DecryptionStageBase, get_tally, ) class DecryptionS2AnnounceService(DecryptionStageBase): """Responsible for the 2nd stage in decryptions where the admin announces guardian decryptions""" def should_run(self, db: Database, decryption: DecryptionDto) -> bool: is_admin = self._auth_service.is_admin() all_guardians_joined = len(decryption.guardians_joined) >= decryption.guardians is_completed = decryption.completed_at_utc is not None return is_admin and all_guardians_joined and not is_completed def run(self, db: Database, decryption: DecryptionDto) -> None: _update_decrypt_status("Starting tally") self._log.info(f"S2: Announcing decryption {decryption.decryption_id}") election = self._election_service.get(db, decryption.election_id) context = election.get_context() decryption_mediator = DecryptionMediator( "decryption-mediator", context, ) decryption_shares = decryption.get_decryption_shares() share_count = len(decryption_shares) current_share = 1 for decryption_share_dict in decryption_shares: _update_decrypt_status(f"Calculating share {current_share}/{share_count}") self._log.debug(f"announcing {decryption_share_dict.guardian_id}") guardian_sequence_number = election.get_guardian_sequence_order( decryption_share_dict.guardian_id ) # coefficients will fail validation unless the key is a numeric encoded # string of the guardian's sequence number decryption_share_dict.guardian_key.owner_id = str(guardian_sequence_number) decryption_mediator.announce( decryption_share_dict.guardian_key, decryption_share_dict.tally_share, decryption_share_dict.ballot_shares, ) current_share += 1 manifest = election.get_manifest() ballots = self._ballot_upload_service.get_ballots( db, election.id, _update_decrypt_status ) spoiled_ballots = [ ballot for ballot in ballots if ballot.state == BallotBoxState.SPOILED ] _update_decrypt_status("Calculating tally") self._log.debug(f"getting tally for {len(ballots)} ballots") ciphertext_tally = get_tally(manifest, context, ballots, False) self._log.debug("getting plaintext tally") plaintext_tally = decryption_mediator.get_plaintext_tally( ciphertext_tally, manifest ) if plaintext_tally is None: raise Exception("No plaintext tally found") self._log.debug("getting plaintext spoiled ballots") _update_decrypt_status("Processing spoiled ballots") plaintext_spoiled_ballots = decryption_mediator.get_plaintext_ballots( spoiled_ballots, manifest ) if plaintext_spoiled_ballots is None: raise Exception("No plaintext spoiled ballots found") _update_decrypt_status("Finalizing tally") lagrange_coefficients = _get_lagrange_coefficients(decryption_mediator) self._log.debug("setting decryption completed") self._decryption_service.set_decryption_completed( db, decryption.decryption_id, plaintext_tally, plaintext_spoiled_ballots, lagrange_coefficients, ciphertext_tally.publish(), ) self._decryption_service.notify_changed(db, decryption.decryption_id) def _get_lagrange_coefficients( decryption_mediator: DecryptionMediator, ) -> LagrangeCoefficientsRecord: return LagrangeCoefficientsRecord(decryption_mediator.get_lagrange_coefficients()) def _update_decrypt_status(status: str) -> None: # pylint: disable=no-member eel.update_decrypt_status(status) # type: ignore[attr-defined] ================================================ FILE: src/electionguard_gui/services/decryption_stages/decryption_stage_base.py ================================================ from abc import ABC from typing import List from pymongo.database import Database from electionguard.ballot import SubmittedBallot from electionguard.election import CiphertextElectionContext from electionguard.manifest import InternalManifest, Manifest from electionguard.tally import CiphertextTally from electionguard.scheduler import Scheduler from electionguard_gui.models.decryption_dto import DecryptionDto from electionguard_gui.services.authorization_service import AuthorizationService from electionguard_gui.services.ballot_upload_service import BallotUploadService from electionguard_gui.services.db_service import DbService from electionguard_gui.services.decryption_service import DecryptionService from electionguard_gui.services.eel_log_service import EelLogService from electionguard_gui.services.election_service import ElectionService from electionguard_gui.services.guardian_service import GuardianService class DecryptionStageBase(ABC): """Responsible for shared functionality across all decryption stages""" _log: EelLogService _db_service: DbService _decryption_service: DecryptionService _auth_service: AuthorizationService _guardian_service: GuardianService _election_service: ElectionService _ballot_upload_service: BallotUploadService def __init__( self, log_service: EelLogService, db_service: DbService, decryption_service: DecryptionService, auth_service: AuthorizationService, guardian_service: GuardianService, election_service: ElectionService, ballot_upload_service: BallotUploadService, ): self._db_service = db_service self._decryption_service = decryption_service self._auth_service = auth_service self._log = log_service self._guardian_service = guardian_service self._election_service = election_service self._ballot_upload_service = ballot_upload_service # pylint: disable=unused-argument def should_run(self, db: Database, decryption: DecryptionDto) -> bool: return False def run(self, db: Database, decryption: DecryptionDto) -> None: pass def get_tally( manifest: Manifest, context: CiphertextElectionContext, ballots: List[SubmittedBallot], should_validate: bool, ) -> CiphertextTally: internal_manifest = InternalManifest(manifest) tally = CiphertextTally( "election-results", internal_manifest, context, ) ballot_tuples = [(None, ballot) for ballot in ballots] with Scheduler() as scheduler: tally.batch_append(ballot_tuples, should_validate, scheduler) return tally ================================================ FILE: src/electionguard_gui/services/directory_service.py ================================================ import os DOCKER_MOUNT_DIR = "/egui_mnt" def get_export_dir() -> str: return _get_egui_mnt_subdir("export") def get_data_dir() -> str: return _get_egui_mnt_subdir("data") def _get_egui_mnt_subdir(subdir_name: str) -> str: egui_mnt_dir = _get_egui_mnt_dir() subdir_path = os.path.join(egui_mnt_dir, subdir_name) os.makedirs(subdir_path, exist_ok=True) return subdir_path def _get_egui_mnt_dir() -> str: # basically if we're in a docker container if os.path.exists(DOCKER_MOUNT_DIR): return DOCKER_MOUNT_DIR return os.path.join(os.getcwd(), "egui_mnt") ================================================ FILE: src/electionguard_gui/services/eel_log_service.py ================================================ from datetime import datetime import logging from os import path, makedirs from typing import Any from electionguard.logs import ( get_file_handler, log_critical, log_debug, log_error, log_info, log_warning, LOG, ) from electionguard_gui.services.directory_service import get_data_dir from electionguard_gui.services.service_base import ServiceBase class EelLogService(ServiceBase): """A facade for logging. Currently this simply writes to the console without using log levels, but this may eventually be used to log to a file or database.""" def __init__(self) -> None: LOG.set_stream_log_level(logging.DEBUG) file_dir = path.join(get_data_dir(), "logs") makedirs(file_dir, exist_ok=True) now = datetime.now().strftime("%Y-%m-%d-%H-%M-%S-%f") file_name = path.join(file_dir, f"electionguard-{now}.log") LOG.add_handler(get_file_handler(logging.DEBUG, file_name)) def trace(self, message: str, *args: Any, **kwargs: Any) -> None: pass def debug(self, message: str, *args: Any, **kwargs: Any) -> None: log_debug(message, *args, **kwargs) def info(self, message: str, *args: Any, **kwargs: Any) -> None: log_info(message, *args, **kwargs) def warn(self, message: str, *args: Any, **kwargs: Any) -> None: log_warning(message, *args, **kwargs) def error(self, message: str, exception: Exception) -> None: log_error( f"{message} '{exception}'", exc_info=1, extra={"exception": exception}, ) def fatal(self, message: str, exception: Exception) -> None: log_critical( f"{message} '{str(exception)}'", exc_info=1, extra={"exception": exception}, ) ================================================ FILE: src/electionguard_gui/services/election_service.py ================================================ import json from datetime import datetime, timezone from bson import ObjectId from pymongo.database import Database from electionguard.constants import ElectionConstants from electionguard.election import CiphertextElectionContext from electionguard.guardian import GuardianRecord from electionguard.manifest import Manifest from electionguard.serialize import to_raw from electionguard_gui.models import KeyCeremonyDto, ElectionDto from electionguard_gui.services.eel_log_service import EelLogService from electionguard_gui.services.service_base import ServiceBase from electionguard_gui.services.authorization_service import AuthorizationService class ElectionService(ServiceBase): """Responsible for functionality related to elections""" _log: EelLogService _auth_service: AuthorizationService def __init__( self, log_service: EelLogService, auth_service: AuthorizationService ) -> None: self._log = log_service self._auth_service = auth_service def create_election( self, db: Database, election_name: str, key_ceremony: KeyCeremonyDto, manifest: Manifest, context: CiphertextElectionContext, constants: ElectionConstants, guardian_records: list[GuardianRecord], encryption_package_file: str, election_url: str, ) -> str: context_raw = to_raw(context) manifest_raw = to_raw(manifest) constants_raw = to_raw(constants) guardian_records_raw = to_raw(guardian_records) election = { "election_name": election_name, "key_ceremony_id": key_ceremony.id, "guardians": context.number_of_guardians, "quorum": context.quorum, "election_url": election_url, "manifest": { "raw": manifest_raw, "name": manifest.get_name(), "scope": manifest.election_scope_id, "geopolitical_units": len(manifest.geopolitical_units), "parties": len(manifest.parties), "candidates": len(manifest.candidates), "contests": len(manifest.contests), "ballot_styles": len(manifest.ballot_styles), }, "context": context_raw, "constants": constants_raw, "guardian_records": guardian_records_raw, # Mongo has a max size of 16MG, consider using GridFS https://www.mongodb.com/docs/manual/core/gridfs/ "encryption_package_file": encryption_package_file, "ballot_uploads": [], "decryptions": [], "created_by": self._auth_service.get_user_id(), "created_at": datetime.now(timezone.utc), } self._log.trace(f"inserting election: {election}") inserted_id = db.elections.insert_one(election).inserted_id return str(inserted_id) def get(self, db: Database, election_id: str) -> ElectionDto: self._log.trace(f"getting election {election_id}") election = db.elections.find_one({"_id": ObjectId(election_id)}) if not election: raise Exception(f"Election not found: {election_id}") return ElectionDto(election) def get_all(self, db: Database) -> list[ElectionDto]: self._log.trace("getting all elections") elections = db.elections.find() return [ElectionDto(election) for election in elections] def append_ballot_upload( self, db: Database, election_id: str, ballot_upload_id: str, device_file_contents: str, created_at: datetime, ) -> None: self._log.trace( f"appending ballot upload {ballot_upload_id} to election {election_id}" ) device_file_json = json.loads(device_file_contents) db.elections.update_one( {"_id": ObjectId(election_id)}, { "$push": { "ballot_uploads": { "ballot_upload_id": ballot_upload_id, "device_file_contents": device_file_contents, "device_id": device_file_json["device_id"], "launch_code": device_file_json["launch_code"], "location": device_file_json["location"], "session_id": device_file_json["session_id"], "ballot_count": 0, "created_at": created_at, } } }, ) def append_decryption( self, db: Database, election_id: str, decryption_id: str, name: str ) -> None: self._log.trace( f"appending decryption {decryption_id} to election {election_id}" ) db.elections.update_one( {"_id": ObjectId(election_id)}, { "$push": { "decryptions": { "decryption_id": decryption_id, "name": name, "created_at": datetime.now(timezone.utc), } } }, ) def increment_ballot_upload_ballot_count( self, db: Database, election_id: str, ballot_upload_id: str ) -> None: self._log.trace( f"incrementing ballot upload {ballot_upload_id} ballot count in election {election_id}" ) db.elections.update_one( { "_id": ObjectId(election_id), "ballot_uploads.ballot_upload_id": ballot_upload_id, }, {"$inc": {"ballot_uploads.$.ballot_count": 1}}, ) ================================================ FILE: src/electionguard_gui/services/export_service.py ================================================ import os from electionguard_gui.services.directory_service import get_data_dir, get_export_dir def get_export_locations() -> list[str]: export_dir = get_export_dir() if os.name == "nt": drives = get_removable_drives() return [export_dir, _get_download_path(), get_data_dir()] + drives return [export_dir] def get_removable_drives() -> list[str]: dl = "DEFGHIJKLMNOPQRSTUVWXYZ" drives = [f"{d}:\\" for d in dl if os.path.exists(f"{d}:")] return drives def _get_download_path() -> str: """ Returns the default downloads path for linux or windows. Code from https://pyquestions.com/python-finding-the-user-s-downloads-folder """ if os.name == "nt": # pylint: disable=import-outside-toplevel # pylint: disable=import-error import winreg # type: ignore[import-not-found] sub_key = ( r"SOFTWARE\\Microsoft\Windows\\CurrentVersion\\Explorer\\Shell Folders" ) downloads_guid = "{374DE290-123F-4565-9164-39C4925E467B}" with winreg.OpenKey( # type: ignore[attr-defined] winreg.HKEY_CURRENT_USER, sub_key # type: ignore[attr-defined] ) as key: location = winreg.QueryValueEx( # type: ignore[attr-defined] key, downloads_guid )[0] return str(location) return os.path.join(os.path.expanduser("~"), "downloads") ================================================ FILE: src/electionguard_gui/services/guardian_service.py ================================================ from os import path from electionguard.serialize import from_file, to_file from electionguard.guardian import Guardian, PrivateGuardianRecord from electionguard.key_ceremony import CeremonyDetails from electionguard.key_ceremony_mediator import KeyCeremonyMediator from electionguard_gui.models.decryption_dto import DecryptionDto from electionguard_gui.models.key_ceremony_dto import KeyCeremonyDto from electionguard_gui.services.directory_service import get_data_dir from electionguard_gui.services.eel_log_service import EelLogService from electionguard_gui.services.service_base import ServiceBase from electionguard_tools.helpers.export import GUARDIAN_PREFIX class GuardianService(ServiceBase): """Responsible for functionality related to guardians""" _log: EelLogService def __init__(self, log_service: EelLogService) -> None: self._log = log_service def save_guardian(self, guardian: Guardian, key_ceremony: KeyCeremonyDto) -> None: private_guardian_record = guardian.export_private_data() file_name = GUARDIAN_PREFIX + private_guardian_record.guardian_id file_path = path.join(get_data_dir(), "gui_private_keys", key_ceremony.id) file = to_file(private_guardian_record, file_name, file_path) self._log.warn( f"Guardian private data saved to {file}. This data should be carefully protected and never shared." ) def _load_guardian( self, guardian_id: str, key_ceremony_id: str, guardian_count: int, quorum: int ) -> Guardian: file_name = GUARDIAN_PREFIX + guardian_id + ".json" file_path = path.join( get_data_dir(), "gui_private_keys", key_ceremony_id, file_name ) self._log.debug(f"loading guardian from {file_path}") if not path.exists(file_path): raise Exception(f"Guardian file not found: {file_path}") private_guardian_record = from_file(PrivateGuardianRecord, file_path) return Guardian.from_private_record( private_guardian_record, guardian_count, quorum, ) def load_guardian_from_decryption( self, guardian_id: str, decryption: DecryptionDto ) -> Guardian: if not decryption.key_ceremony_id: raise Exception("key_ceremony_id is required") return self._load_guardian( guardian_id, decryption.key_ceremony_id, decryption.guardians, decryption.quorum, ) def load_guardian_from_key_ceremony( self, guardian_id: str, key_ceremony: KeyCeremonyDto ) -> Guardian: return self._load_guardian( guardian_id, key_ceremony.id, key_ceremony.guardian_count, key_ceremony.quorum, ) def load_other_keys( self, key_ceremony: KeyCeremonyDto, current_user_id: str, guardian: Guardian ) -> None: current_user_other_keys = key_ceremony.find_other_keys_for_user(current_user_id) for other_key in current_user_other_keys: other_user = other_key.owner_id self._log.debug(f"saving other_key from {other_user} for {current_user_id}") guardian.save_guardian_key(other_key) def make_guardian( user_id: str, guardian_number: int, key_ceremony: KeyCeremonyDto ) -> Guardian: return Guardian.from_nonce( user_id, guardian_number, key_ceremony.guardian_count, key_ceremony.quorum, ) def make_mediator(key_ceremony: KeyCeremonyDto) -> KeyCeremonyMediator: quorum = key_ceremony.quorum guardian_count = key_ceremony.guardian_count ceremony_details = CeremonyDetails(guardian_count, quorum) mediator: KeyCeremonyMediator = KeyCeremonyMediator("mediator_1", ceremony_details) return mediator def announce_guardians( key_ceremony: KeyCeremonyDto, mediator: KeyCeremonyMediator ) -> None: for guardian_id in key_ceremony.guardians_joined: key = key_ceremony.find_key(guardian_id) mediator.announce(key) ================================================ FILE: src/electionguard_gui/services/gui_setup_input_retrieval_step.py ================================================ from typing import Optional from electionguard import Manifest from electionguard.guardian import Guardian from electionguard_cli import SetupInputs from electionguard_cli.setup_election import SetupInputRetrievalStep class GuiSetupInputRetrievalStep(SetupInputRetrievalStep): """Responsible for retrieving and parsing user provided inputs for the GUI's setup election command.""" def get_gui_inputs( self, guardian_count: int, quorum: int, guardians: list[Guardian], verification_url: Optional[str], manifest_raw: str, ) -> SetupInputs: self.print_header("Retrieving Inputs") manifest: Manifest = self._get_manifest_raw(manifest_raw) return SetupInputs( guardian_count, quorum, guardians, manifest, verification_url, force=True ) ================================================ FILE: src/electionguard_gui/services/key_ceremony_service.py ================================================ from typing import Any, List from datetime import datetime, timezone from pymongo.database import Database from bson import ObjectId from electionguard.key_ceremony import ( ElectionJointKey, ElectionPartialKeyBackup, ElectionPartialKeyVerification, ElectionPublicKey, ) from electionguard_gui.models.key_ceremony_dto import KeyCeremonyDto from electionguard_gui.services.authorization_service import AuthorizationService from electionguard_gui.services.db_serialization_service import ( backup_to_dict, joint_key_to_dict, public_key_to_dict, verification_to_dict, ) from electionguard_gui.services.db_watcher_service import DbWatcherService from electionguard_gui.services.eel_log_service import EelLogService from electionguard_gui.services.service_base import ServiceBase class KeyCeremonyService(ServiceBase): """Responsible for functionality related to key ceremonies""" _log: EelLogService _auth_service: AuthorizationService _db_watcher_service: DbWatcherService def __init__( self, log_service: EelLogService, auth_service: AuthorizationService, db_watcher_service: DbWatcherService, ) -> None: self._log = log_service self._auth_service = auth_service self._db_watcher_service = db_watcher_service def create( self, db: Database, key_ceremony_name: str, guardian_count: int, quorum: int ) -> str: key_ceremony = { "key_ceremony_name": key_ceremony_name, "guardian_count": guardian_count, "quorum": quorum, "guardians_joined": [], "keys": [], "guardians_keys": [], "other_keys": [], "backups": [], "shared_backups": [], "verifications": [], "joint_key": None, "created_by": self._auth_service.get_user_id(), "created_at": datetime.now(timezone.utc), "completed_at": None, } inserted_id = db.key_ceremonies.insert_one(key_ceremony).inserted_id self._log.debug(f"created '{key_ceremony_name}' record, id: {inserted_id}") return str(inserted_id) def notify_changed(self, db: Database, key_ceremony_id: str) -> None: self._db_watcher_service.notify_changed(db, "key_ceremonies", key_ceremony_id) def get(self, db: Database, id: str) -> KeyCeremonyDto: key_ceremony_dict = db.key_ceremonies.find_one({"_id": ObjectId(id)}) if key_ceremony_dict is None: raise ValueError(f"key ceremony '{id}' not found") dto = KeyCeremonyDto(key_ceremony_dict) dto.set_can_join(self._auth_service) return dto def append_guardian_joined( self, db: Database, key_ceremony_id: str, guardian_id: str ) -> None: db.key_ceremonies.update_one( {"_id": ObjectId(key_ceremony_id)}, {"$push": {"guardians_joined": guardian_id}}, ) def append_key( self, db: Database, key_ceremony_id: str, key: ElectionPublicKey ) -> None: db.key_ceremonies.update_one( {"_id": ObjectId(key_ceremony_id)}, {"$push": {"keys": public_key_to_dict(key)}}, ) def append_other_key(self, db: Database, key_ceremony_id: str, keys: Any) -> None: db.key_ceremonies.update_one( {"_id": ObjectId(key_ceremony_id)}, {"$push": {"other_keys": {"$each": keys}}}, ) def append_backups( self, db: Database, key_ceremony_id: str, backups: List[ElectionPartialKeyBackup], ) -> None: backups_dict = [backup_to_dict(backup) for backup in backups] db.key_ceremonies.update_one( {"_id": ObjectId(key_ceremony_id)}, {"$push": {"backups": {"$each": backups_dict}}}, ) def append_shared_backups( self, db: Database, key_ceremony_id: str, shared_backups: List[Any], ) -> None: db.key_ceremonies.update_one( {"_id": ObjectId(key_ceremony_id)}, {"$push": {"shared_backups": {"$each": shared_backups}}}, ) def append_verifications( self, db: Database, key_ceremony_id: str, verifications: List[ElectionPartialKeyVerification], ) -> None: verifications_dict = [ verification_to_dict(verification) for verification in verifications ] db.key_ceremonies.update_one( {"_id": ObjectId(key_ceremony_id)}, {"$push": {"verifications": {"$each": verifications_dict}}}, ) def append_joint_key( self, db: Database, key_ceremony_id: str, joint_key: ElectionJointKey, ) -> None: joint_key_dict = joint_key_to_dict(joint_key) db.key_ceremonies.update_one( {"_id": ObjectId(key_ceremony_id)}, {"$set": {"joint_key": joint_key_dict}}, ) def set_complete( self, db: Database, key_ceremony_id: str, ) -> None: db.key_ceremonies.update_one( {"_id": ObjectId(key_ceremony_id)}, {"$set": {"completed_at": datetime.now(timezone.utc)}}, ) def get_completed(self, db: Database) -> List[KeyCeremonyDto]: key_ceremonies = db.key_ceremonies.find({"completed_at": {"$ne": None}}) return [KeyCeremonyDto(key_ceremony) for key_ceremony in key_ceremonies] def get_active(self, db: Database) -> List[KeyCeremonyDto]: key_ceremonies = db.key_ceremonies.find({"completed_at": {"$eq": None}}) return [KeyCeremonyDto(key_ceremony) for key_ceremony in key_ceremonies] def exists(self, db: Database, key_ceremony_name: str) -> bool: existing_key_ceremonies = db.key_ceremonies.find_one( {"key_ceremony_name": key_ceremony_name} ) return existing_key_ceremonies is not None def get_guardian_number(key_ceremony: KeyCeremonyDto, guardian_id: str) -> int: """Returns the position of a guardian within the array of guardians that have joined a key ceremony. This technique is important because it avoids concurrency problems that could arise if simply retrieving the number of guardians""" guardian_num = 1 for guardian in key_ceremony.guardians_joined: if guardian == guardian_id: return guardian_num guardian_num += 1 raise ValueError(f"guardian '{guardian_id}' not found") ================================================ FILE: src/electionguard_gui/services/key_ceremony_stages/__init__.py ================================================ from electionguard_gui.services.key_ceremony_stages import key_ceremony_s1_join_service from electionguard_gui.services.key_ceremony_stages import ( key_ceremony_s2_announce_service, ) from electionguard_gui.services.key_ceremony_stages import ( key_ceremony_s3_make_backup_service, ) from electionguard_gui.services.key_ceremony_stages import ( key_ceremony_s4_share_backup_service, ) from electionguard_gui.services.key_ceremony_stages import ( key_ceremony_s5_verify_backup_service, ) from electionguard_gui.services.key_ceremony_stages import ( key_ceremony_s6_publish_key_service, ) from electionguard_gui.services.key_ceremony_stages import key_ceremony_stage_base from electionguard_gui.services.key_ceremony_stages.key_ceremony_s1_join_service import ( KeyCeremonyS1JoinService, ) from electionguard_gui.services.key_ceremony_stages.key_ceremony_s2_announce_service import ( KeyCeremonyS2AnnounceService, ) from electionguard_gui.services.key_ceremony_stages.key_ceremony_s3_make_backup_service import ( KeyCeremonyS3MakeBackupService, ) from electionguard_gui.services.key_ceremony_stages.key_ceremony_s4_share_backup_service import ( KeyCeremonyS4ShareBackupService, ) from electionguard_gui.services.key_ceremony_stages.key_ceremony_s5_verify_backup_service import ( KeyCeremonyS5VerifyBackupService, ) from electionguard_gui.services.key_ceremony_stages.key_ceremony_s6_publish_key_service import ( KeyCeremonyS6PublishKeyService, ) from electionguard_gui.services.key_ceremony_stages.key_ceremony_stage_base import ( KeyCeremonyStageBase, ) __all__ = [ "KeyCeremonyS1JoinService", "KeyCeremonyS2AnnounceService", "KeyCeremonyS3MakeBackupService", "KeyCeremonyS4ShareBackupService", "KeyCeremonyS5VerifyBackupService", "KeyCeremonyS6PublishKeyService", "KeyCeremonyStageBase", "key_ceremony_s1_join_service", "key_ceremony_s2_announce_service", "key_ceremony_s3_make_backup_service", "key_ceremony_s4_share_backup_service", "key_ceremony_s5_verify_backup_service", "key_ceremony_s6_publish_key_service", "key_ceremony_stage_base", ] ================================================ FILE: src/electionguard_gui/services/key_ceremony_stages/key_ceremony_s1_join_service.py ================================================ from pymongo.database import Database from electionguard_gui.models.key_ceremony_dto import KeyCeremonyDto from electionguard_gui.services.key_ceremony_service import get_guardian_number from electionguard_gui.services.key_ceremony_stages.key_ceremony_stage_base import ( KeyCeremonyStageBase, ) from electionguard_gui.services.guardian_service import make_guardian class KeyCeremonyS1JoinService(KeyCeremonyStageBase): """Responsible for stage 1 of the key ceremony where guardians join""" def run(self, db: Database, key_ceremony: KeyCeremonyDto) -> None: key_ceremony_id = key_ceremony.id user_id = self._auth_service.get_required_user_id() self._key_ceremony_service.append_guardian_joined(db, key_ceremony_id, user_id) # refresh key ceremony to get the list of guardians with the authoritative order they joined in key_ceremony = self._key_ceremony_service.get(db, key_ceremony_id) guardian_number = get_guardian_number(key_ceremony, user_id) self.log.debug( f"user {user_id} about to join key ceremony {key_ceremony_id} as guardian #{guardian_number}" ) guardian = make_guardian(user_id, guardian_number, key_ceremony) self._guardian_service.save_guardian(guardian, key_ceremony) public_key = guardian.share_key() self._key_ceremony_service.append_key(db, key_ceremony_id, public_key) self.log.debug( f"{user_id} joined key ceremony {key_ceremony_id} as guardian #{guardian_number}" ) self._key_ceremony_service.notify_changed(db, key_ceremony_id) ================================================ FILE: src/electionguard_gui/services/key_ceremony_stages/key_ceremony_s2_announce_service.py ================================================ from typing import Any, List from pymongo.database import Database from electionguard.key_ceremony import ElectionPublicKey from electionguard.utils import get_optional from electionguard_gui.models.key_ceremony_dto import KeyCeremonyDto from electionguard_gui.models.key_ceremony_states import KeyCeremonyStates from electionguard_gui.services.db_serialization_service import public_key_to_dict from electionguard_gui.services.guardian_service import ( announce_guardians, make_mediator, ) from electionguard_gui.services.key_ceremony_stages.key_ceremony_stage_base import ( KeyCeremonyStageBase, ) class KeyCeremonyS2AnnounceService(KeyCeremonyStageBase): """Responsible for stage 2 of the key ceremony where admins announce the key ceremony""" def should_run( self, key_ceremony: KeyCeremonyDto, state: KeyCeremonyStates ) -> bool: is_admin = self._auth_service.is_admin() should_run: bool = is_admin and state == KeyCeremonyStates.PendingAdminAnnounce return should_run def run(self, db: Database, key_ceremony: KeyCeremonyDto) -> None: key_ceremony_id = key_ceremony.id self.log.info("all guardians have joined, announcing guardians") other_keys = self.announce(key_ceremony) self.log.debug("saving other_keys") self._key_ceremony_service.append_other_key(db, key_ceremony_id, other_keys) self._key_ceremony_service.notify_changed(db, key_ceremony_id) def announce(self, key_ceremony: KeyCeremonyDto) -> List[dict[str, Any]]: other_keys = [] mediator = make_mediator(key_ceremony) announce_guardians(key_ceremony, mediator) for guardian_id in key_ceremony.guardians_joined: self.log.debug(f"announcing guardian {guardian_id}") other_guardian_keys: List[ElectionPublicKey] = get_optional( mediator.share_announced(guardian_id) ) other_keys.append( { "owner_id": guardian_id, "other_keys": [ public_key_to_dict(key) for key in other_guardian_keys ], } ) return other_keys ================================================ FILE: src/electionguard_gui/services/key_ceremony_stages/key_ceremony_s3_make_backup_service.py ================================================ from pymongo.database import Database from electionguard_gui.models.key_ceremony_dto import KeyCeremonyDto from electionguard_gui.models.key_ceremony_states import KeyCeremonyStates from electionguard_gui.services.key_ceremony_stages.key_ceremony_stage_base import ( KeyCeremonyStageBase, ) class KeyCeremonyS3MakeBackupService(KeyCeremonyStageBase): """Responsible for stage 3 of the key ceremony where guardians create backups to send to the admin.""" def should_run( self, key_ceremony: KeyCeremonyDto, state: KeyCeremonyStates ) -> bool: is_guardian = not self._auth_service.is_admin() current_user_id = self._auth_service.get_required_user_id() current_user_backups = key_ceremony.get_backup_count_for_user(current_user_id) current_user_backup_exists = current_user_backups > 0 return ( is_guardian and state == KeyCeremonyStates.PendingGuardianBackups and not current_user_backup_exists ) def run(self, db: Database, key_ceremony: KeyCeremonyDto) -> None: current_user_id = self._auth_service.get_required_user_id() key_ceremony_id = key_ceremony.id self.log.debug(f"creating backups for guardian {current_user_id}") guardian = self._guardian_service.load_guardian_from_key_ceremony( current_user_id, key_ceremony ) self._guardian_service.load_other_keys(key_ceremony, current_user_id, guardian) guardian.generate_election_partial_key_backups() backups = guardian.share_election_partial_key_backups() self._key_ceremony_service.append_backups(db, key_ceremony_id, backups) # notify the admin that a new guardian has backups self._key_ceremony_service.notify_changed(db, key_ceremony_id) ================================================ FILE: src/electionguard_gui/services/key_ceremony_stages/key_ceremony_s4_share_backup_service.py ================================================ from typing import Any, List from pymongo.database import Database from electionguard_gui.models.key_ceremony_dto import KeyCeremonyDto from electionguard_gui.models.key_ceremony_states import KeyCeremonyStates from electionguard_gui.services.db_serialization_service import backup_to_dict from electionguard_gui.services.guardian_service import ( announce_guardians, make_mediator, ) from electionguard_gui.services.key_ceremony_stages.key_ceremony_stage_base import ( KeyCeremonyStageBase, ) class KeyCeremonyS4ShareBackupService(KeyCeremonyStageBase): """ Responsible for stage 4 of the key ceremony where admins receive backups and share them back to guardians for verification. """ def should_run( self, key_ceremony: KeyCeremonyDto, state: KeyCeremonyStates ) -> bool: is_admin: bool = self._auth_service.is_admin() return is_admin and state == KeyCeremonyStates.PendingAdminToShareBackups def run(self, db: Database, key_ceremony: KeyCeremonyDto) -> None: current_user_id = self._auth_service.get_user_id() self.log.debug(f"sharing backups for admin {current_user_id}") shared_backups = self.share_backups(key_ceremony) self._key_ceremony_service.append_shared_backups( db, key_ceremony.id, shared_backups ) self._key_ceremony_service.notify_changed(db, key_ceremony.id) def share_backups(self, key_ceremony: KeyCeremonyDto) -> List[Any]: mediator = make_mediator(key_ceremony) announce_guardians(key_ceremony, mediator) mediator.receive_backups(key_ceremony.get_backups()) shared_backups = [] for guardian_id in key_ceremony.guardians_joined: self.log.debug(f"sharing backups for guardian {guardian_id}") guardian_backups = mediator.share_backups(guardian_id) if guardian_backups is None: raise Exception("Error sharing backups") backups_as_dict = [backup_to_dict(backup) for backup in guardian_backups] shared_backups.append({"owner_id": guardian_id, "backups": backups_as_dict}) return shared_backups ================================================ FILE: src/electionguard_gui/services/key_ceremony_stages/key_ceremony_s5_verify_backup_service.py ================================================ from typing import List from pymongo.database import Database from electionguard.key_ceremony import ElectionPartialKeyVerification from electionguard_gui.models.key_ceremony_dto import KeyCeremonyDto from electionguard_gui.models.key_ceremony_states import KeyCeremonyStates from electionguard_gui.services.key_ceremony_stages.key_ceremony_stage_base import ( KeyCeremonyStageBase, ) class KeyCeremonyS5VerifyBackupService(KeyCeremonyStageBase): """Responsible for stage 5 of the key ceremony where guardians verify backups.""" def should_run( self, key_ceremony: KeyCeremonyDto, state: KeyCeremonyStates ) -> bool: is_guardian = not self._auth_service.is_admin() current_user_id = self._auth_service.get_required_user_id() current_user_verifications = key_ceremony.get_verification_count_for_user( current_user_id ) current_user_verification_exists = current_user_verifications > 0 return ( is_guardian and state == KeyCeremonyStates.PendingGuardiansVerifyBackups and not current_user_verification_exists ) def run(self, db: Database, key_ceremony: KeyCeremonyDto) -> None: current_user_id = self._auth_service.get_required_user_id() shared_backups = key_ceremony.get_shared_backups_for_guardian(current_user_id) guardian = self._guardian_service.load_guardian_from_key_ceremony( current_user_id, key_ceremony ) self._guardian_service.load_other_keys(key_ceremony, current_user_id, guardian) verifications: List[ElectionPartialKeyVerification] = [] for backup in shared_backups: self.log.debug( f"verifying backup from {backup.owner_id} to {current_user_id}" ) guardian.save_election_partial_key_backup(backup) verification = guardian.verify_election_partial_key_backup(backup.owner_id) if verification is None: raise Exception("Error verifying backup") verifications.append(verification) self._key_ceremony_service.append_verifications( db, key_ceremony.id, verifications ) # notify the admin that a new verification was created self._key_ceremony_service.notify_changed(db, key_ceremony.id) ================================================ FILE: src/electionguard_gui/services/key_ceremony_stages/key_ceremony_s6_publish_key_service.py ================================================ from pymongo.database import Database from electionguard_gui.models.key_ceremony_dto import KeyCeremonyDto from electionguard_gui.models.key_ceremony_states import KeyCeremonyStates from electionguard_gui.services.guardian_service import ( announce_guardians, make_mediator, ) from electionguard_gui.services.key_ceremony_stages.key_ceremony_stage_base import ( KeyCeremonyStageBase, ) class KeyCeremonyS6PublishKeyService(KeyCeremonyStageBase): """ Responsible for stage 6 of the key ceremony where admins receive verifications, publish a joint key, and generate a context. """ def should_run( self, key_ceremony: KeyCeremonyDto, state: KeyCeremonyStates ) -> bool: is_admin = self._auth_service.is_admin() return is_admin and state == KeyCeremonyStates.PendingAdminToPublishJointKey def run(self, db: Database, key_ceremony: KeyCeremonyDto) -> None: current_user_id = self._auth_service.get_user_id() self.log.debug(f"receiving verifications for admin {current_user_id}") mediator = make_mediator(key_ceremony) announce_guardians(key_ceremony, mediator) mediator.receive_backups(key_ceremony.get_backups()) verifications = key_ceremony.get_verifications() mediator.receive_backup_verifications(verifications) election_joint_key = mediator.publish_joint_key() if election_joint_key is None: raise Exception("Failed to publish joint key") self.log.info(f"joint key published: {election_joint_key.joint_public_key}") self._key_ceremony_service.append_joint_key( db, key_ceremony.id, election_joint_key ) self._key_ceremony_service.set_complete(db, key_ceremony.id) # notify everyone that verifications completed and the joint key published self._key_ceremony_service.notify_changed(db, key_ceremony.id) ================================================ FILE: src/electionguard_gui/services/key_ceremony_stages/key_ceremony_stage_base.py ================================================ from abc import ABC, abstractmethod from pymongo.database import Database from electionguard_gui.models.key_ceremony_dto import KeyCeremonyDto from electionguard_gui.models.key_ceremony_states import KeyCeremonyStates from electionguard_gui.services.authorization_service import AuthorizationService from electionguard_gui.services.db_service import DbService from electionguard_gui.services.eel_log_service import EelLogService from electionguard_gui.services.guardian_service import GuardianService from electionguard_gui.services.key_ceremony_service import KeyCeremonyService from electionguard_gui.services.key_ceremony_state_service import ( KeyCeremonyStateService, ) class KeyCeremonyStageBase(ABC): """Responsible for shared functionality across all key ceremony stages""" log: EelLogService _db_service: DbService _key_ceremony_service: KeyCeremonyService _auth_service: AuthorizationService _key_ceremony_state_service: KeyCeremonyStateService _guardian_service: GuardianService def __init__( self, log_service: EelLogService, db_service: DbService, key_ceremony_service: KeyCeremonyService, auth_service: AuthorizationService, key_ceremony_state_service: KeyCeremonyStateService, guardian_service: GuardianService, ): self._db_service = db_service self._key_ceremony_service = key_ceremony_service self._auth_service = auth_service self._key_ceremony_state_service = key_ceremony_state_service self.log = log_service self._guardian_service = guardian_service @abstractmethod def should_run( self, key_ceremony: KeyCeremonyDto, state: KeyCeremonyStates ) -> bool: raise NotImplementedError @abstractmethod def run(self, db: Database, key_ceremony: KeyCeremonyDto) -> None: raise NotImplementedError ================================================ FILE: src/electionguard_gui/services/key_ceremony_state_service.py ================================================ from electionguard_gui.models.key_ceremony_dto import KeyCeremonyDto from electionguard_gui.models.key_ceremony_states import KeyCeremonyStates from electionguard_gui.services.eel_log_service import EelLogService from electionguard_gui.services.service_base import ServiceBase class KeyCeremonyStateService(ServiceBase): """Responsible for determining the state of the key ceremony""" log: EelLogService def __init__(self, log_service: EelLogService) -> None: self.log = log_service # pylint: disable=too-many-return-statements def get_key_ceremony_state(self, key_ceremony: KeyCeremonyDto) -> KeyCeremonyStates: guardians_joined = len(key_ceremony.guardians_joined) guardian_count = key_ceremony.guardian_count other_keys = len(key_ceremony.other_keys) backups = len(key_ceremony.backups) shared_backups = len(key_ceremony.shared_backups) expected_backups = pow(guardian_count, 2) verifications = len(key_ceremony.verifications) expected_verifications = pow(guardian_count, 2) - guardian_count self.log.debug( f"guardians: {guardians_joined}/{guardian_count}; " + f"other_keys: {other_keys}/{guardian_count}; " + f"backups: {backups}/{expected_backups}; " + f"shared_backups: {shared_backups}/{guardian_count}; " + f"verifications: {verifications}/{expected_verifications}" ) if guardians_joined < guardian_count: return KeyCeremonyStates.PendingGuardiansJoin if other_keys == 0: return KeyCeremonyStates.PendingAdminAnnounce if backups < expected_backups: return KeyCeremonyStates.PendingGuardianBackups if shared_backups == 0: return KeyCeremonyStates.PendingAdminToShareBackups if verifications < expected_verifications: return KeyCeremonyStates.PendingGuardiansVerifyBackups if not key_ceremony.joint_key_exists(): return KeyCeremonyStates.PendingAdminToPublishJointKey return KeyCeremonyStates.Complete status_descriptions = { KeyCeremonyStates.PendingGuardiansJoin: "waiting for guardians", KeyCeremonyStates.PendingAdminAnnounce: "waiting for admin to announce guardians", KeyCeremonyStates.PendingGuardianBackups: "waiting for guardians to create backups", KeyCeremonyStates.PendingAdminToShareBackups: "waiting for admin to share backups", KeyCeremonyStates.PendingGuardiansVerifyBackups: "waiting for guardians to verify backups", KeyCeremonyStates.PendingAdminToPublishJointKey: "waiting for admin to publish the joint key", KeyCeremonyStates.Complete: "key ceremony complete", } def get_key_ceremony_status(state: KeyCeremonyStates) -> str: return status_descriptions[state] ================================================ FILE: src/electionguard_gui/services/plaintext_ballot_service.py ================================================ from typing import Any from electionguard import PlaintextTally from electionguard.manifest import Manifest, get_i8n_value from electionguard.tally import PlaintextTallySelection from electionguard_gui.models.election_dto import ElectionDto def get_plaintext_ballot_report( election: ElectionDto, plaintext_ballot: PlaintextTally ) -> list: manifest = election.get_manifest() selection_names = manifest.get_selection_names("en") contest_names = manifest.get_contest_names() selection_write_ins = _get_candidate_write_ins(manifest) parties = _get_selection_parties(manifest) tally_report = _get_tally_report( plaintext_ballot, selection_names, contest_names, selection_write_ins, parties ) return tally_report def _get_tally_report( plaintext_ballot: PlaintextTally, selection_names: dict[str, str], contest_names: dict[str, str], selection_write_ins: dict[str, bool], parties: dict[str, str], ) -> list: tally_report = [] contests = plaintext_ballot.contests.values() for tally_contest in contests: selections = list(tally_contest.selections.values()) contest_details = _get_contest_details( selections, selection_names, selection_write_ins, parties ) contest_name = contest_names.get(tally_contest.object_id, "n/a") tally_report.append( { "name": contest_name, "details": contest_details, } ) return tally_report def _get_contest_details( selections: list[PlaintextTallySelection], selection_names: dict[str, str], selection_write_ins: dict[str, bool], parties: dict[str, str], ) -> dict[str, Any]: # non-write-in selections non_write_in_selections = [ selection for selection in selections if not selection_write_ins[selection.object_id] ] non_write_in_total = sum(selection.tally for selection in non_write_in_selections) non_write_in_selections_report = _get_selections_report( non_write_in_selections, selection_names, parties, non_write_in_total ) # write-in selections write_ins = [ selection.tally for selection in selections if selection_write_ins[selection.object_id] ] any_write_ins = len(write_ins) > 0 write_ins_total = sum(write_ins) if any_write_ins else None return { "selections": non_write_in_selections_report, "nonWriteInTotal": non_write_in_total, "writeInTotal": write_ins_total, } def _get_selection_parties(manifest: Manifest) -> dict[str, str]: parties = { party.object_id: get_i8n_value(party.name, "en", "") for party in manifest.parties } candidates = { candidate.object_id: parties[candidate.party_id] for candidate in manifest.candidates if candidate.party_id is not None } contest_parties = {} for contest in manifest.contests: for selection in contest.ballot_selections: party = candidates.get(selection.candidate_id, "") contest_parties[selection.object_id] = party return contest_parties def _get_candidate_write_ins(manifest: Manifest) -> dict[str, bool]: """ Returns a dictionary where the key is a selection's object_id and the value is a boolean indicating whether the selection's candidate is marked as a write-in. """ write_in_candidates = { candidate.object_id: candidate.is_write_in is True for candidate in manifest.candidates } contest_write_ins = {} for contest in manifest.contests: for selection in contest.ballot_selections: candidate_is_write_in = write_in_candidates[selection.candidate_id] contest_write_ins[selection.object_id] = candidate_is_write_in return contest_write_ins def _get_selections_report( selections: list[PlaintextTallySelection], selection_names: dict[str, str], parties: dict[str, str], total: int, ) -> list: selections_report = [] for selection in selections: selection_name = selection_names[selection.object_id] party = parties.get(selection.object_id, "") percent: float = ( (float(selection.tally) / total) if selection.tally else float(0) ) selections_report.append( { "name": selection_name, "tally": selection.tally, "party": party, "percent": percent, } ) return selections_report ================================================ FILE: src/electionguard_gui/services/service_base.py ================================================ from abc import ABC class ServiceBase(ABC): """Responsible for common functionality among services""" def init(self) -> None: self.expose() def expose(self) -> None: pass ================================================ FILE: src/electionguard_gui/services/version_service.py ================================================ from os import path from subprocess import check_output from typing import Optional import eel from electionguard_gui.services.eel_log_service import EelLogService from electionguard_gui.services.service_base import ServiceBase class VersionService(ServiceBase): """Responsible for retrieving version information""" _log: EelLogService def __init__(self, log_service: EelLogService) -> None: self._log = log_service def expose(self) -> None: eel.expose(self.get_version) def get_version(self) -> Optional[str]: if not path.exists(".git"): return None commit_hash = ( check_output(["git", "rev-parse", "--short", "HEAD"]) .decode("ascii") .strip() ) self._log.info(f"Version: {commit_hash}") return commit_hash ================================================ FILE: src/electionguard_gui/start.py ================================================ from electionguard_gui.containers import Container def run() -> None: container = Container() container.main_app().start() ================================================ FILE: src/electionguard_gui/web/components/admin/admin-home-component.js ================================================ import KeyCeremonyList from "../shared/key-ceremony-list-component.js"; import ElectionsList from "../shared/election-list-component.js"; export default { components: { KeyCeremonyList, ElectionsList, }, data() { return { loading: true, keyCeremonies: [], }; }, async mounted() { const result = await eel.get_key_ceremonies()(); if (result.success) { this.keyCeremonies = result.result; } else { console.error(result.error); } this.loading = false; }, template: /*html*/ ` <div class="container col-md-6"> <div class="text-center mb-4"> <h1>Admin Menu</h1> </div> <div class="row justify-content-md-center"> <div class="col-12 d-grid mb-3"> <a href="#/admin/create-key-ceremony" class="btn btn-primary">Create Key Ceremony</a> </div> <div class="col-12 d-grid mb-3"> <a href="#/admin/create-election" class="btn btn-primary">Create Election</a> </div> </div> </div> <div class="text-center mt-4"> <elections-list></elections-list> </div> <div class="text-center mt-4"> <key-ceremony-list :show-when-empty="false" :is-admin="true" :key-ceremonies="keyCeremonies"></key-ceremony-list> </div> `, }; ================================================ FILE: src/electionguard_gui/web/components/admin/create-decryption-component.js ================================================ import RouterService from "../../services/router-service.js"; import Spinner from "../shared/spinner-component.js"; export default { props: { electionId: String, }, components: { Spinner }, data() { return { name: "", alert: undefined, loading: false, }; }, methods: { async createDecryption() { console.log("createDecryption"); this.loading = true; const result = await eel.create_decryption(this.electionId, this.name)(); if (result.success) { RouterService.goTo(RouterService.routes.viewDecryptionAdmin, { decryptionId: result.result, }); } else { this.alert = result.message; } this.loading = false; }, getElectionUrl: function () { return RouterService.getElectionUrl(this.electionId); }, }, async mounted() { const result = await eel.get_suggested_decryption_name(this.electionId)(); if (result.success) { this.name = result.result; } else { result.alert = result.message; } }, template: /*html*/ ` <div v-if="alert" class="alert alert-danger" role="alert"> {{ alert }} </div> <form id="mainForm" class="needs-validation" novalidate @submit.prevent="createDecryption"> <div class="row g-3 text-center col-6 mx-auto"> <div class="col-12"> <h1>Create Tally</h1> </div> <div class="col-12"> <label for="name" class="form-label">Name</label> <input type="text" id="name" class="form-control" v-model="name" required> </div> <div class="col-12 mt-4"> <a :href="getElectionUrl()" class="btn btn-secondary">Cancel</a> <button type="submit" class="btn btn-primary ms-3">Create</button> <spinner :visible="loading"></spinner> </div> </div> </form> `, }; ================================================ FILE: src/electionguard_gui/web/components/admin/create-election-component.js ================================================ import Spinner from "../shared/spinner-component.js"; import RouterService from "../../services/router-service.js"; export default { data() { return { loading: false, alert: null, electionName: "", electionUrl: "", keys: [], }; }, components: { Spinner, }, methods: { async createElection() { const form = document.getElementById("mainForm"); if (form.checkValidity()) { self.loading = true; self.alert = null; const [manifest] = document.getElementById("manifest").files; const manifestContent = await manifest.text(); const result = await eel.create_election( this.electionKey.id, this.electionName, manifestContent, this.electionUrl )(); console.log("creating election"); this.loading = false; console.log("creating election completed", result); if (result.success) { RouterService.goTo(RouterService.routes.viewElectionAdmin, { electionId: result.result, }); } else { this.alert = result.message; } } form.classList.add("was-validated"); }, keyChanged() { if (!this.electionName) { this.electionName = this.electionKey.key_ceremony_name; } }, }, async mounted() { const result = await eel.get_keys()(); if (result.success) { this.keys = result.result; } else { console.error(result.message); } }, template: /*html*/ ` <form id="mainForm" class="needs-validation" novalidate @submit.prevent="createElection"> <div v-if="alert" class="alert alert-danger" role="alert"> {{ alert }} </div> <div class="row g-3 align-items-center"> <div class="col-12"> <h1>Create Election</h1> </div> <div class="col-sm-12"> <label for="electionKey" class="form-label">Key</label> <select id="electionKey" class="form-select" v-model="electionKey" @change="keyChanged()"> <option v-for="key in keys" :value="key">{{ key.key_ceremony_name }}</option> </select> </div> <div class="col-sm-12"> <label for="electionName" class="form-label">Name</label> <input id="electionName" type="text" class="form-control" v-model="electionName" required /> <div class="invalid-feedback">Please provide an election name.</div> </div> <div class="col-12"> <label for="manifest" class="form-label">Manifest</label> <input type="file" id="manifest" class="form-control" required /> <div class="invalid-feedback">Please provide a valid manifest.</div> </div> <div class="col-sm-12"> <label for="electionUrl" class="form-label">Election URL</label> <input id="electionUrl" type="text" class="form-control" v-model="electionUrl" /> </div> <div class="col-12 mt-4"> <button type="submit" class="btn btn-primary">Create Election</button> <spinner :visible="loading"></spinner> </div> </div> </form>`, }; ================================================ FILE: src/electionguard_gui/web/components/admin/create-key-ceremony-component.js ================================================ import Spinner from "../shared/spinner-component.js"; import RouterService from "../../services/router-service.js"; export default { components: { Spinner, }, data() { return { loading: false, alert: null, keyCeremonyName: "", guardianCount: 2, quorum: 2, }; }, methods: { startCeremony() { const form = document.getElementById("mainForm"); this.alert = null; self.alert = null; if (form.checkValidity()) { this.loading = true; const onDone = eel.create_key_ceremony( this.keyCeremonyName, this.guardianCount, this.quorum ); onDone((result) => { this.loading = false; console.debug("key ceremony creation finished", result); if (result.success) { RouterService.goTo(RouterService.routes.viewKeyCeremonyAdminPage, { keyCeremonyId: result.result, }); } else { this.alert = result.message; } }); } form.classList.add("was-validated"); }, }, template: /*html*/ ` <form id="mainForm" class="needs-validation" novalidate @submit.prevent="startCeremony"> <div v-if="alert" class="alert alert-danger" role="alert"> {{ alert }} </div> <div class="row g-3 align-items-center"> <div class="col-12"> <h1>Create Key Ceremony</h1> </div> <div class="col-sm-12"> <label for="keyCeremonyName" class="form-label">Key Ceremony Name</label> <input type="text" class="form-control" id="keyCeremonyName" v-model="keyCeremonyName" required min="2" /> <div class="invalid-feedback"> Key Ceremony Name is required </div> </div> <div class="col-sm-6"> <label for="guardianCount" class="form-label">Number of Guardians</label> <input type="number" class="form-control" id="guardianCount" v-model="guardianCount" required min="2" /> <div class="invalid-feedback"> Please provide a valid number of guardians. </div> </div> <div class="col-sm-6"> <label for="quorum" class="form-label">Quorum</label> <input type="number" class="form-control" id="quorum" v-model="quorum" required /> <div class="invalid-feedback">Please provide a valid quorum.</div> </div> <div class="col-12 mt-4"> <button type="submit" class="btn btn-primary" :disabled="loading">Start Ceremony</button> <spinner :visible="loading"></spinner> </div> </div> </form>`, }; ================================================ FILE: src/electionguard_gui/web/components/admin/export-election-record-component.js ================================================ import RouterService from "../../services/router-service.js"; import Spinner from "../shared/spinner-component.js"; export default { props: { decryptionId: String, }, components: { Spinner }, data() { return { locations: [], location: null, loading: false, alert: undefined, success: false, }; }, methods: { async exportRecord() { this.loading = true; this.alert = undefined; const result = await eel.export_election_record( this.decryptionId, this.location )(); this.loading = false; this.success = result.success; if (!result.success) { console.error(result.message); this.alert = "An error occurred exporting the election record."; } }, getDecryptionUrl: function () { return RouterService.getUrl(RouterService.routes.viewDecryptionAdmin, { decryptionId: this.decryptionId, }); }, }, async mounted() { this.alert = undefined; const result = await eel.get_election_record_export_locations()(); if (result.success) { this.locations = result.result; this.location = this.locations[0]; } else { console.error(result.message); this.alert = "An error occurred while loading the export locations."; } }, template: /*html*/ ` <div v-if="alert" class="alert alert-danger" role="alert"> {{ alert }} </div> <form id="mainForm" class="needs-validation" novalidate @submit.prevent="exportRecord" v-if="!success"> <div class="row g-3 align-items-center"> <div class="col-12"> <h1>Election Record</h1> </div> <div class="col-12"> <label for="location" class="form-label">Location</label> <select id="location" class="form-select" v-model="location"> <option v-for="location in locations" :value="location">{{ location }}</option> </select> </div> <div class="col-12 mt-4"> <button type="submit" class="btn btn-primary">Export</button> <a :href="getDecryptionUrl()" class="btn btn-secondary ms-3">Cancel</a> </div> <div class="col-12"> <spinner :visible="loading"></spinner> </div> </div> </form> <div v-if="success" class="text-center"> <img src="/images/check.svg" width="200" height="200" class="mt-4 mb-2"></img> <p>The election record has been exported to {{ location }}.</p> <a :href="getDecryptionUrl()" class="btn btn-primary">Continue</a> </div> `, }; ================================================ FILE: src/electionguard_gui/web/components/admin/export-encryption-package-component.js ================================================ import RouterService from "../../services/router-service.js"; import Spinner from "../shared/spinner-component.js"; export default { props: { electionId: String, }, components: { Spinner }, data() { return { locations: [], location: null, loading: false, alert: undefined, success: false, }; }, methods: { async exportPackage() { this.loading = true; this.alert = undefined; const result = await eel.export_encryption_package( this.electionId, this.location )(); this.loading = false; this.success = result.success; if (!result.success) { console.error(result.message); this.alert = "An error occurred exporting the encryption package."; } }, getElectionUrl: function () { return RouterService.getElectionUrl(this.electionId); }, }, async mounted() { this.alert = undefined; const result = await eel.get_encryption_package_export_locations()(); if (result.success) { this.locations = result.result; this.location = this.locations[0]; } else { console.error(result.message); this.alert = "An error occurred while loading the export locations."; } }, template: /*html*/ ` <div v-if="alert" class="alert alert-danger" role="alert"> {{ alert }} </div> <form id="mainForm" class="needs-validation" novalidate @submit.prevent="exportPackage" v-if="!success"> <div class="row g-3 align-items-center"> <div class="col-12"> <h1>Encryption Package</h1> </div> <div class="col-12"> <label for="location" class="form-label">Location</label> <select id="location" class="form-select" v-model="location"> <option v-for="location in locations" :value="location">{{ location }}</option> </select> </div> <div class="col-12 mt-4"> <a :href="getElectionUrl()" class="btn btn-secondary">Cancel</a> <button type="submit" class="btn btn-primary ms-3">Export</button> <spinner :visible="loading"></spinner> </div> </div> </form> <div v-if="success" class="text-center"> <img src="/images/check.svg" width="200" height="200" class="mt-4 mb-2"></img> <p>The encryption package has been exported to {{ location }}.</p> <a :href="getElectionUrl()" class="btn btn-primary">Continue</a> </div> `, }; ================================================ FILE: src/electionguard_gui/web/components/admin/upload-ballots-component.js ================================================ import UploadBallotsLegacy from "./upload-ballots-legacy-component.js"; import UploadBallotsWizard from "./upload-ballots-wizard-component.js"; export default { props: { electionId: String, }, methods: { closeWizard: function () { this.useWizard = false; }, }, data() { return { useWizard: null, }; }, components: { UploadBallotsLegacy, UploadBallotsWizard, }, async mounted() { this.useWizard = await eel.is_wizard_supported()(); }, template: /*html*/ ` <upload-ballots-legacy :election-id="electionId" v-if="useWizard !== null && !useWizard"></upload-ballots-legacy> <upload-ballots-wizard @close="closeWizard" :election-id="electionId" v-if="useWizard !== null && useWizard"></upload-ballots-wizard> `, }; ================================================ FILE: src/electionguard_gui/web/components/admin/upload-ballots-legacy-component.js ================================================ import RouterService from "../../services/router-service.js"; import Spinner from "../shared/spinner-component.js"; import UploadBallotsSuccess from "./upload-ballots-success-component.js"; export default { props: { electionId: String, }, components: { Spinner, UploadBallotsSuccess }, data() { return { election: null, loading: false, alert: null, ballotsProcessed: null, ballotsTotal: null, success: false, duplicateCount: 0, }; }, methods: { async uploadBallots() { try { const form = document.getElementById("mainForm"); if (form.checkValidity()) { this.loading = true; this.alert = null; this.ballotsProcessed = 0; const ballotFiles = document.getElementById("ballotsFolder").files; this.ballotsTotal = ballotFiles.length; const uploadId = await this.uploadDeviceFile(); await this.uploadBallotFiles(uploadId, ballotFiles); this.success = true; } form.classList.add("was-validated"); } catch (ex) { console.error(ex); this.alert = ex.message; } finally { this.loading = false; } }, async uploadDeviceFile() { const [deviceFile] = document.getElementById("deviceFile").files; const deviceContents = await deviceFile.text(); console.log("Creating election", deviceFile.name); const result = await eel.create_ballot_upload( this.electionId, deviceFile.name, deviceContents )(); if (!result.success) { throw new Error(result.message); } this.ballotsProcessed++; return result.result; }, async uploadBallotFiles(uploadId, ballotFiles) { for (let i = 0; i < ballotFiles.length; i++) { const ballotFile = ballotFiles[i]; const ballotContents = await ballotFile.text(); console.log("Uploading ballot", ballotFile.name); const result = await eel.upload_ballot( uploadId, this.electionId, ballotFile.name, ballotContents )(); if (!result.success) { throw new Error(result.message); } if (result.result.is_duplicate) { this.duplicateCount++; } this.ballotsProcessed++; } }, getElectionUrl: function () { return RouterService.getElectionUrl(this.electionId); }, uploadMore: function () { this.success = false; this.duplicateCount = 0; this.election = null; this.loading = false; this.alert = null; this.ballotsProcessed = null; this.ballotsTotal = null; this.$nextTick(() => { this.resetFiles(); }); }, resetFiles: function () { document.getElementById("deviceFile").value = null; document.getElementById("ballotsFolder").value = null; }, }, mounted() { this.resetFiles(); }, template: /*html*/ ` <div v-if="alert" class="alert alert-danger" role="alert"> {{ alert }} </div> <div v-if="duplicateCount" class="alert alert-warning" role="alert"> {{ duplicateCount }} ballots were skipped because their object_ids had already been uploaded for this election. </div> <form id="mainForm" class="needs-validation" novalidate @submit.prevent="uploadBallots" v-if="!success"> <div class="row g-3 align-items-center"> <div class="col-12"> <h1>Upload Ballots</h1> </div> <div class="col-12"> <label for="deviceFile" class="form-label">Device File</label> <input type="file" id="deviceFile" class="form-control" required /> <div class="invalid-feedback">Please provide a device file.</div> </div> <div class="col-12"> <label for="ballotsFolder" class="form-label">Ballot Folder</label> <input type="file" id="ballotsFolder" class="form-control" webkitdirectory directory required /> <div class="invalid-feedback">Please provide a ballot folder.</div> </div> <div class="col-12 mt-4"> <button type="submit" :disabled="loading" class="btn btn-primary me-2">Upload</button> <a :href="getElectionUrl()" class="btn btn-secondary me-2">Cancel</a> <spinner :visible="loading"></spinner> <p v-if="loading && ballotsProcessed">{{ ballotsProcessed }} of {{ ballotsTotal }} files processed.</p> </div> </form> <upload-ballots-success v-if="success" :back-url="getElectionUrl()" @upload-more="uploadMore()" :ballot-count="ballotsTotal-duplicateCount"></upload-ballots-success> `, }; ================================================ FILE: src/electionguard_gui/web/components/admin/upload-ballots-success-component.js ================================================ export default { props: { ballotCount: Number, electionId: String, backUrl: String, }, template: /*html*/ ` <div class="text-center"> <img src="/images/check.svg" width="200" height="200" class="mt-4 mb-2"></img> <p>Successfully uploaded {{ballotCount}} ballots.</p> <button type="button" @click="$emit('uploadMore')" class="btn btn-secondary me-2">Upload More Ballots</button> <a :href="backUrl" class="btn btn-primary">Done Uploading</a> </div> `, }; ================================================ FILE: src/electionguard_gui/web/components/admin/upload-ballots-wizard-component.js ================================================ import RouterService from "../../services/router-service.js"; import Spinner from "../shared/spinner-component.js"; import UploadBallotsSuccess from "./upload-ballots-success-component.js"; export default { props: { electionId: String, }, data() { return { drive: null, success: false, alert: null, ballotCount: null, status: null, loading: false, duplicateCount: 0, }; }, methods: { uploadBallots: async function () { this.loading = true; try { const result = await eel.upload_ballots(this.electionId)(); console.log("upload completed", result); if (result.success) { this.success = true; this.ballotCount = result.result.ballot_count; this.duplicateCount = result.result.duplicate_count; } else { this.alert = result.message; } } finally { this.loading = false; this.status = null; } }, closeWizard: function () { this.$emit("close"); }, getElectionUrl: function () { return RouterService.getElectionUrl(this.electionId); }, uploadMore: async function () { this.success = false; this.drive = null; this.alert = null; this.ballotCount = null; this.duplicateCount = 0; this.status = null; this.loading = false; await this.scanDrives(); }, scanDrives: async function () { const result = await eel.scan_drives()(); if (!result.success) { console.error(result.message); this.alert = result.message; } else { this.drive = result.result; console.log("successfully uploaded ballots", this.drive); } }, updateUploadStatus: function (status) { console.log("updateUploadStatus", status); this.status = status; }, pollDrives: async function () { if (this.drive) return; await this.scanDrives(); if (!this.drive) { // keep polling until a valid drive is found setTimeout(this.pollDrives.bind(this), 1000); } }, }, async mounted() { eel.expose(this.updateUploadStatus, "update_upload_status"); await this.pollDrives(); }, components: { UploadBallotsSuccess, Spinner, }, template: /*html*/ ` <div v-if="alert" class="alert alert-danger" role="alert"> {{ alert }} </div> <div v-if="duplicateCount" class="alert alert-warning" role="alert"> {{ duplicateCount }} ballots were skipped because their object_ids had already been uploaded for this election. </div> <upload-ballots-success v-if="success" :back-url="getElectionUrl()" @upload-more="uploadMore()" :ballot-count="ballotCount"></upload-ballots-success> <div v-else> <div class="row"> <div class="col-md-12 text-end"> <button type="button" class="btn btn-sm btn-default" @click="closeWizard"> <i class="bi bi-x-square"></i> </button> </div> </div> <div class="text-center"> <h1>Upload Wizard</h1> <div v-if="!drive"> <p>Insert a USB drive containing ballots</p> <spinner class="mt-4" :visible="true"></spinner> </div> <div v-if="drive"> <p class="mt-4">Ready to import?</p> <div class="row g-1"> <div class="col-6 fw-bold text-end"> {{drive.drive}} </div> <div class="col-6 text-start"> drive </div> </div> <div class="row g-1"> <div class="col-6 fw-bold text-end"> {{drive.ballots}} </div> <div class="col-6 text-start"> ballots </div> </div> <div class="row g-1"> <div class="col-6 fw-bold text-end"> {{drive.location}} </div> <div class="col-6 text-start"> device </div> </div> <div class="mt-4"> <a :href="getElectionUrl()" class="btn btn-secondary me-2">Cancel</a> <button class="btn btn-primary" :disabled="loading" @click="uploadBallots">Import</button> <p class="mt-3" v-if="status">{{ status }}</p> <spinner class="mt-4" :visible="loading"></spinner> </div> </div> </div> </div> `, }; ================================================ FILE: src/electionguard_gui/web/components/admin/view-decryption-admin-component.js ================================================ import RouterService from "../../services/router-service.js"; import Spinner from "../shared/spinner-component.js"; export default { props: { decryptionId: String, }, components: { Spinner }, data() { return { decryption: null, loading: false, error: false, status: null }; }, methods: { getElectionUrl: function (electionId) { return RouterService.getElectionUrl(electionId); }, getExportElectionRecordUrl: function () { return RouterService.getUrl(RouterService.routes.exportElectionRecord, { decryptionId: this.decryptionId, }); }, getViewTallyUrl: function () { return RouterService.getUrl(RouterService.routes.viewTally, { decryptionId: this.decryptionId, }); }, getSpoiledBallotUrl: function (spoiledBallotId) { return RouterService.getUrl(RouterService.routes.viewSpoiledBallot, { decryptionId: this.decryptionId, spoiledBallotId: spoiledBallotId, }); }, refresh_decryption: async function (result) { if (result.success) { await this.get_decryption(true); } else { console.error(result.message); this.error = true; this.loading = false; this.status = null; this.decryption = null; } }, get_decryption: async function (is_refresh) { console.log("getting decryption"); this.loading = true; try { const result = await eel.get_decryption( this.decryptionId, is_refresh )(); console.log("get_decryption complete", result); this.error = !result.success; this.decryption = result.success ? result.result : null; } catch (error) { console.error(error); } finally { this.loading = false; this.status = null; } }, updateDecryptStatus: function (status) { this.status = status; }, }, async mounted() { eel.expose(this.refresh_decryption, "refresh_decryption"); eel.expose(this.updateDecryptStatus, "update_decrypt_status"); await this.get_decryption(false); console.log("watching decryption"); // only watch for changes if the decryption is in-progress if (this.decryption && !this.decryption.completed_at_str) { eel.watch_decryption(this.decryptionId); } }, unmounted() { console.log("stop watching decryption"); eel.stop_watching_decryption(); }, template: /*html*/ ` <div v-if="error"> <p class="alert alert-danger" role="alert">An error occurred. Check the logs and/or <a href="javascript:history.back()">try again</a>.</p> </div> <div v-if="decryption"> <div class="text-end"> <div v-if="decryption.completed_at_str"> <div class="dropdown"> <button class="btn btn-sm btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false"> <i class="bi-gear-fill me-1"></i> </button> <ul class="dropdown-menu"> <li> <a :href="getExportElectionRecordUrl()" class="dropdown-item"> <i class="bi-download me-1"></i> Download election record </a> </li> <li> <a :href="getViewTallyUrl()" class="dropdown-item" v-if="decryption.completed_at_str"> <i class="bi-card-text me-1"></i> View Tally </a> </li> </ul> </div> </div> </div> <div class="row"> <h1>{{decryption.decryption_name}}</h1> </div> <div class="row"> <div class="col col-12 col-md-6 col-lg-5"> <div class="col-md-8"> <dt>Election</dt> <dd><a :href="getElectionUrl(decryption.election_id)">{{decryption.election_name}}</a></dd> </div> <div class="mb-4"> <div class="row"> <div class="col-md-6"> <dt>Ballot Uploads</dt> <dd>{{decryption.ballot_upload_count}}</dd> </div> <div class="col-md-6"> <dt>Total Ballots</dt> <dd>{{decryption.ballot_count}}</dd> </div> </div> <dl class="col-12"> <dt>Created</dt> <dd>by {{decryption.created_by}} on {{decryption.created_at}}</dd> </dl> <dl class="col-12" v-if="decryption.completed_at_str"> <dt>Completed</dt> <dd>{{decryption.completed_at_str}}</dd> </dl> </div> <div class="col-12 mb-4"> <h3>Joined Guardians</h3> <ul v-if="decryption.guardians_joined.length"> <li v-for="guardian in decryption.guardians_joined">{{guardian}}</li> </ul> <div v-else> <p>No guardians have joined yet</p> </div> </div> <div v-if="decryption.completed_at_str" class="mb-4"> <h3>Tally Results</h3> <a :href="getViewTallyUrl()" class="btn btn-sm btn-secondary m-2"><i class="bi bi-binoculars-fill me-1"></i> View Tally</a> </div> <div v-if="decryption.completed_at_str"> <h3>Spoiled Ballots</h3> <table class="table table-striped" v-if="decryption.spoiled_ballots.length"> <thead> <tr> <th>Ballot ID</th> </tr> </thead> <tbody class="table-group-divider"> <tr v-for="spoiledBallot in decryption.spoiled_ballots"> <td><a :href="getSpoiledBallotUrl(spoiledBallot)">{{spoiledBallot}}</a></td> </tr> </tbody> </table> <div v-else> <p>No spoiled ballots existed at the time this tally was run.</p> </div> </div> </div> <div class="col col-12 col-md-6 col-lg-7 text-center"> <img v-if="decryption.completed_at_str" src="/images/check.svg" width="150" height="150" class="mb-2"></img> <p class="key-ceremony-status">{{decryption.status}}</p> <p class="mt-3" v-if="status">{{ status }}</p> <spinner :visible="loading || !decryption.completed_at_str"></spinner> </div> </div> </div> <div v-else> <spinner :visible="loading"></spinner> </div> `, }; ================================================ FILE: src/electionguard_gui/web/components/admin/view-election-component.js ================================================ import RouterService from "../../services/router-service.js"; import Spinner from "../shared/spinner-component.js"; export default { props: { electionId: String, }, components: { Spinner }, data() { return { election: null, loading: false, error: false, ballotSum: 0 }; }, methods: { getEncryptionPackageUrl: function () { const page = RouterService.routes.exportEncryptionPackage; return RouterService.getUrl(page, { electionId: this.electionId, }); }, getUploadBallotsUrl: function () { const page = RouterService.routes.uploadBallots; return RouterService.getUrl(page, { electionId: this.electionId, }); }, getCreateDecryptionUrl: function () { const page = RouterService.routes.createDecryption; return RouterService.getUrl(page, { electionId: this.electionId, }); }, getViewDecryptionUrl: function (decryptionId) { const page = RouterService.routes.viewDecryptionAdmin; return RouterService.getUrl(page, { decryptionId: decryptionId, }); }, setBallotSum: function (election) { this.ballotSum = 0; for (const spoiledBallot of election.ballot_uploads) { this.ballotSum += spoiledBallot.ballot_count; } }, }, async mounted() { const result = await eel.get_election(this.electionId)(); if (result.success) { this.election = result.result; this.setBallotSum(this.election); } else { this.error = true; } }, template: /*html*/ ` <div v-if="election"> <div class="container"> <div class="text-end"> <div class="dropdown"> <button class="btn btn-sm btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false"> <i class="bi-gear-fill me-1"></i> </button> <ul class="dropdown-menu"> <li> <a :href="getEncryptionPackageUrl()" class="dropdown-item"> <i class="bi-download me-1"></i> Download encryption package </a> </li> </ul> </div> </div> <div class="row"> <div class="col col-12 col-md-8"> <h1>{{election.election_name}}</h1> <div class="row mb-4"> <dl class="col-md-6 mb-1"> <dt>Guardians</dt> <dd>{{election.guardians}}</dd> </dl> <dl class="col-md-6 mb-1"> <dt>Quorum</dt> <dd>{{election.quorum}}</dd> </dl> <dl class="col-md-12 mb-1" v-if="election.election_url"> <dt>Election URL</dt> <dd>{{election.election_url}}</dd> </dl> <dl class="col-12 mb-1"> <dt>Created</dt> <dd>by {{election.created_by}} on {{election.created_at}}</dd> </dl> </div> <div class="row mb-4"> <div class="col-12"> <h2>Ballots</h2> <table class="table table-striped" v-if="election.ballot_uploads.length"> <thead> <tr> <th>Uploaded</th> <th>Location</th> <th>Ballot Count</th> </tr> </thead> <tbody class="table-group-divider"> <tr v-for="ballot_upload in election.ballot_uploads"> <td>{{ballot_upload.created_at}}</td> <td>{{ballot_upload.location}}</td> <td>{{ballot_upload.ballot_count}}</td> </tr> </tbody> <tfoot> <tr class="table-secondary"> <td><em>Total</em></td> <td> </td> <td>{{ballotSum}} </tr> </tfoot> </table> <div v-else> <p>No ballots have been added yet.</p> </div> <div> <a :href="getUploadBallotsUrl()" class="btn btn-sm btn-secondary"> <i class="bi-plus bi-plus me-2"></i> Add Ballots </a> </div> </div> </div> <div class="row mb-4" v-if="election.ballot_uploads.length"> <div class="col-12"> <h2>Tallies</h2> <table class="table table-striped" v-if="election.decryptions.length"> <thead> <tr> <th>Created</th> <th>Name</th> </tr> </thead> <tbody class="table-group-divider"> <tr v-for="decryption in election.decryptions"> <td>{{decryption.created_at}}</td> <td><a :href="getViewDecryptionUrl(decryption.decryption_id)">{{decryption.name}}</a></td> </tr> </tbody> </table> <div v-else> <p>No tallies have been created yet.</p> </div> <a :href="getCreateDecryptionUrl()" class="btn btn-sm btn-secondary"> <i class="bi bi-people-fill me-1"></i> Create tally </a> </div> </div> </div> <div class="col col-12 col-md-4"> <h2>Manifest</h2> <p>{{election.manifest.name}}</p> <div class="row"> <dl class="col-md-6"> <dt>Parties</dt> <dd>{{election.manifest.parties}}</dd> </dl> <dl class="col-md-6"> <dt>Candidates</dt> <dd>{{election.manifest.candidates}}</dd> </dl> <dl class="col-md-6"> <dt>Contests</dt> <dd>{{election.manifest.contests}}</dd> </dl> <dl class="col-md-6"> <dt>Ballot Styles</dt> <dd>{{election.manifest.ballot_styles}}</dd> </dl> <dl class="col-md-12"> <dt>Geopolitical Units</dt> <dd>{{election.manifest.geopolitical_units}}</dd> </dl> </div> </div> </div> </div> </div> <div v-else> <spinner :visible="loading"></spinner> <div v-if="error"> <p class="alert alert-danger" role="alert">An error occurred with the election. Check the logs and try again.</p> </div> </div> `, }; ================================================ FILE: src/electionguard_gui/web/components/admin/view-key-ceremony-component.js ================================================ import KeyCeremonyDetails from "../shared/key-ceremony-details-component.js"; export default { props: { keyCeremonyId: String, }, components: { KeyCeremonyDetails, }, template: /*html*/ ` <key-ceremony-details :keyCeremonyId="keyCeremonyId"></key-ceremony-details> `, }; ================================================ FILE: src/electionguard_gui/web/components/admin/view-spoiled-ballot-component.js ================================================ import RouterService from "../../services/router-service.js"; import Spinner from "../shared/spinner-component.js"; import ViewPlaintextBallotComponent from "../shared/view-plaintext-ballot-component.js"; export default { props: { decryptionId: String, spoiledBallotId: String, }, components: { Spinner, ViewPlaintextBallotComponent }, data() { return { spoiled_ballot: null, loading: true }; }, methods: { getElectionUrl: function (electionId) { return RouterService.getElectionUrl(electionId); }, getDecryptionUrl: function () { return RouterService.getUrl(RouterService.routes.viewDecryptionAdmin, { decryptionId: this.decryptionId, }); }, }, async mounted() { const result = await eel.get_spoiled_ballot( this.decryptionId, this.spoiledBallotId )(); if (result.success) { this.spoiled_ballot = result.result; } else { console.error(result.error); } this.loading = false; }, template: /*html*/ ` <div v-if="spoiled_ballot" class="row"> <div class="col col-12 mb-3"> <a :href="getElectionUrl(spoiled_ballot.election_id)">{{spoiled_ballot.election_name}}</a> > <a :href="getDecryptionUrl()">{{spoiled_ballot.decryption_name}}</a> > {{this.spoiledBallotId}} </div> <div class="col-md-12"> <view-plaintext-ballot-component :ballot="spoiled_ballot.report"></view-plaintext-ballot-component> </div> </div> <spinner :visible="loading"></spinner> `, }; ================================================ FILE: src/electionguard_gui/web/components/admin/view-tally-component.js ================================================ import RouterService from "../../services/router-service.js"; import Spinner from "../shared/spinner-component.js"; import ViewPlaintextBallotComponent from "../shared/view-plaintext-ballot-component.js"; export default { props: { decryptionId: String, }, components: { Spinner, ViewPlaintextBallotComponent }, data() { return { tally: null, loading: true }; }, methods: { getElectionUrl: function (electionId) { return RouterService.getElectionUrl(electionId); }, getDecryptionUrl: function () { return RouterService.getUrl(RouterService.routes.viewDecryptionAdmin, { decryptionId: this.decryptionId, }); }, }, async mounted() { const result = await eel.get_tally(this.decryptionId)(); if (result.success) { this.tally = result.result; } else { console.error(result.error); } this.loading = false; }, template: /*html*/ ` <div v-if="tally" class="row"> <div class="col col-12 mb-3"> <a :href="getElectionUrl(tally.election_id)">{{tally.election_name}}</a> > <a :href="getDecryptionUrl()">{{tally.decryption_name}}</a> > Tally </div> <div class="col-md-12"> <view-plaintext-ballot-component :ballot="tally.report"></view-plaintext-ballot-component> </div> </div> <spinner :visible="loading"></spinner> `, }; ================================================ FILE: src/electionguard_gui/web/components/guardian/decryption-list-component.js ================================================ import RouterService from "../../services/router-service.js"; export default { props: { decryptions: Array, }, data() { return { loading: true, }; }, methods: { getDecryptionUrl: function (decryption) { return RouterService.getUrl(RouterService.routes.viewDecryptionGuardian, { decryptionId: decryption.id, }); }, }, template: /*html*/ ` <h2>Decryptions</h2> <div v-if="!decryptions.length"> <p>No decryptions found.</p> </div> <div v-if="decryptions.length" class="d-grid gap-2 d-md-block"> <a :href="getDecryptionUrl(decryption)" v-for="decryption in decryptions" class="btn btn-primary me-2 mt-2">{{ decryption.decryption_name }}</a> </div> `, }; ================================================ FILE: src/electionguard_gui/web/components/guardian/guardian-home-component.js ================================================ import KeyCeremonyList from "../shared/key-ceremony-list-component.js"; import DecryptionList from "./decryption-list-component.js"; import Spinner from "../shared/spinner-component.js"; const sleepResumeInterval = 2000; export default { components: { KeyCeremonyList, DecryptionList, Spinner, }, data() { return { loading: true, decryptions: [], keyCeremonies: [], lastAwakeTime: new Date().getTime(), }; }, methods: { refresh: async function () { this.loading = true; try { this.decryptions = []; this.keyCeremonies = []; await this.refreshDecryptions(); await this.refreshKeyCeremonies(); } finally { this.loading = false; } }, keyCeremoniesChanged: async function () { await this.refreshKeyCeremonies(); }, decryptionsChanged: async function () { await this.refreshDecryptions(); }, refreshDecryptions: async function () { const result = await eel.get_decryptions()(); if (result.success) { this.decryptions = result.result; } else { console.error(result.error); } }, refreshKeyCeremonies: async function () { const result = await eel.get_key_ceremonies()(); if (result.success) { this.keyCeremonies = result.result; } else { console.error(result.error); } }, sleepResumeChecker: function () { var currentTime = new Date().getTime(); if (currentTime > this.lastAwakeTime + sleepResumeInterval * 2) { console.log("system appears to have returned from sleep, refreshing"); document.location.reload(true); } this.lastAwakeTime = currentTime; }, }, async mounted() { setInterval(this.sleepResumeChecker, sleepResumeInterval); eel.expose(this.keyCeremoniesChanged, "key_ceremonies_changed"); eel.expose(this.decryptionsChanged, "decryptions_changed"); console.log("begin watching for key ceremonies"); eel.watch_db_collections(); await this.refreshKeyCeremonies(); await this.refreshDecryptions(); this.loading = false; }, unmounted() { console.log("stop watching key ceremonies"); eel.stop_watching_db_collections(); clearInterval(this.sleepResumeChecker); }, template: /*html*/ ` <div class="container"> <div class="row"> <div class="col-11"> <h1>Guardian Home</h1> </div> <div class="col-1 text-end"> <button class="btn btn-lg btn-light" @click="refresh"><i class="bi-arrow-clockwise"></i></button> </div> </div> <spinner :visible="loading"></spinner> <div v-if="!loading" class="row"> <div class="col-12 col-lg-6"> <key-ceremony-list :show-when-empty="true" :is-admin="false" :key-ceremonies="keyCeremonies"></key-ceremony-list> </div> <div class="col-12 col-lg-6"> <decryption-list :decryptions="decryptions"></decryption-list> </div> </div> </div> `, }; ================================================ FILE: src/electionguard_gui/web/components/guardian/view-decryption-guardian-component.js ================================================ import RouterService from "../../services/router-service.js"; import Spinner from "../shared/spinner-component.js"; import AuthService from "../../services/authorization-service.js"; export default { props: { decryptionId: String, }, components: { Spinner }, data() { return { decryption: null, loading: false, error: false, successfully_joined: false, status: null, }; }, methods: { getElectionUrl: function (electionId) { return RouterService.getElectionUrl(electionId); }, decrypt: async function () { this.loading = true; try { this.error = false; const result = await eel.join_decryption(this.decryptionId)(); if (result.success) { this.success = true; } else { this.error = true; } } finally { this.loading = false; this.status = null; } }, refresh_decryption: async function () { console.log("refreshing decryption"); this.loading = true; const result = await eel.get_decryption(this.decryptionId, true)(); this.error = !result.success; if (result.success) { this.decryption = result.result; const currentUser = await AuthService.getUserId(); this.successfully_joined = this.decryption.guardians_joined.includes(currentUser); } this.loading = false; }, updateDecryptStatus: function (status) { console.log("updateDecryptStatus", status); this.status = status; }, }, async mounted() { eel.expose(this.updateDecryptStatus, "update_decrypt_status"); eel.expose(this.refresh_decryption, "refresh_decryption"); await this.refresh_decryption(); console.log("watching decryption"); eel.watch_decryption(this.decryptionId); }, unmounted() { console.log("stop watching decryption"); eel.stop_watching_decryption(); }, template: /*html*/ ` <div v-if="error"> <p class="alert alert-danger" role="alert"> ElectionGuard couldn’t perform the action you requested. Ask an Administrator for help, or return to the last screen. </p> </div> <div v-if="decryption"> <div class="row text-center"> <div class="col col-12" v-if="decryption.can_join"> <h1>Join Tally</h1> <p>Click below to join <i>{{decryption.decryption_name}}</i></p> <button @click="decrypt()" :disabled="loading" class="btn btn-primary mb-3">Join</button> <p class="mt-3" v-if="status">{{ status }}</p> <spinner :visible="loading"></spinner> </div> <div class="col col-12" v-if="successfully_joined"> <h1>{{decryption.decryption_name}}</h1> <img src="/images/check.svg" width="200" height="200" class="mb-2"></img> <p class="key-ceremony-status">decryption complete</p> </div> </div> </div> `, }; ================================================ FILE: src/electionguard_gui/web/components/guardian/view-key-ceremony-component.js ================================================ import KeyCeremonyDetails from "../shared/key-ceremony-details-component.js"; export default { props: { keyCeremonyId: String, }, components: { KeyCeremonyDetails, }, template: /*html*/ ` <key-ceremony-details :keyCeremonyId="keyCeremonyId"></key-ceremony-details> `, }; ================================================ FILE: src/electionguard_gui/web/components/shared/election-list-component.js ================================================ import RouterService from "../../services/router-service.js"; import Spinner from "./spinner-component.js"; export default { data() { return { loading: true, elections: [], }; }, components: { Spinner, }, methods: { getElectionUrl: function (election) { return RouterService.getElectionUrl(election.id); }, }, async mounted() { const result = await eel.get_elections()(); this.loading = false; if (result.success) { this.elections = result.result; } else { console.error(result.message); } }, template: /*html*/ ` <spinner :visible="loading"></spinner> <div v-if="elections && elections.length" class="d-grid gap-2 d-md-block"> <h2>Elections</h2> <a :href="getElectionUrl(election)" v-for="election in elections" class="btn btn-primary me-2 mt-2">{{ election.election_name }}</a> </div> `, }; ================================================ FILE: src/electionguard_gui/web/components/shared/footer-component.js ================================================ export default { data() { return { version: null, }; }, async mounted() { this.version = await eel.get_version()(); }, template: /*html*/ ` <nav class="navbar fixed-bottom bg-light" v-if="version"> <div class="container-fluid"> <div class="col-12 pe-0 text-end text-secondary text-opacity-75"> ElectionGuard version {{version}} </div> </div> </nav> `, }; ================================================ FILE: src/electionguard_gui/web/components/shared/home-component.js ================================================ import AuthorizationService from "../../services/authorization-service.js"; import Spinner from "./spinner-component.js"; import AdminHome from "../admin/admin-home-component.js"; import GuardianHome from "../guardian/guardian-home-component.js"; export default { mounted: async function () { this.isAdmin = await AuthorizationService.isAdmin(); }, data() { return { isAdmin: undefined, }; }, components: { Spinner, AdminHome, GuardianHome, }, template: /*html*/ ` <admin-home v-if="isAdmin === true"></admin-home> <guardian-home v-if="isAdmin === false"></guardian-home> <spinner :visible="isAdmin === undefined"></spinner> `, }; ================================================ FILE: src/electionguard_gui/web/components/shared/key-ceremony-details-component.js ================================================ import Spinner from "../shared/spinner-component.js"; export default { props: { keyCeremonyId: String, }, components: { Spinner }, data() { return { keyCeremony: null, loading: false, error: false }; }, methods: { join: async function () { this.loading = true; this.error = false; const result = await eel.join_key_ceremony(this.keyCeremonyId)(); if (!result.success) { this.error = true; } this.loading = false; }, refresh_key_ceremony: function (eelMessage) { console.log("key ceremony refreshed", eelMessage); if (eelMessage.success) { this.keyCeremony = eelMessage.result; } else { console.error(eelMessage.message); this.error = true; this.keyCeremony = undefined; } this.loading = false; }, }, mounted() { eel.expose(this.refresh_key_ceremony, "refresh_key_ceremony"); this.error = false; eel.watch_key_ceremony(this.keyCeremonyId); }, unmounted() { console.log("stop watching key ceremonies"); eel.stop_watching_key_ceremony(); }, template: /*html*/ ` <div v-if="keyCeremony"> <div class="container"> <h1>{{keyCeremony.key_ceremony_name}}</h1> <div class="row"> <div class="col col-12 col-md-6 col-lg-5"> <p>Guardians: {{keyCeremony.guardian_count}}</p> <p>Quorum: {{keyCeremony.quorum}}</p> <p>Created by: {{keyCeremony.created_by}}, {{keyCeremony.created_at_str}}</p> <p v-if="keyCeremony.completed_at_str">Completed: {{keyCeremony.completed_at_str}}</p> <h3>Joined Guardians</h3> <ul v-if="keyCeremony.guardians_joined.length"> <li v-for="guardian in keyCeremony.guardians_joined">{{guardian}}</li> </ul> <div v-else> <p>No guardians have joined yet</p> </div> <button v-if="keyCeremony.can_join" @click="join()" :disabled="loading" class="btn btn-primary">Join</button> </div> <div class="col col-12 col-md-6 col-lg-7 text-center"> <img v-if="keyCeremony.completed_at_str" src="/images/check.svg" width="200" height="200" class="mb-2"></img> <p class="key-ceremony-status">{{keyCeremony.status}}</p> <spinner :visible="loading || !keyCeremony.completed_at_str"></spinner> </div> </div> </div> </div> <div v-else> <div v-if="loading"> <spinner></spinner> </div> <div v-if="error"> <p class="alert alert-danger" role="alert">An error occurred with the key ceremony. Check the logs and try again.</p> </div> </div> `, }; ================================================ FILE: src/electionguard_gui/web/components/shared/key-ceremony-list-component.js ================================================ import RouterService from "../../services/router-service.js"; import Spinner from "./spinner-component.js"; export default { props: { isAdmin: Boolean, showWhenEmpty: Boolean, keyCeremonies: Array, }, data() { return { loading: true, }; }, components: { Spinner, }, methods: { getKeyCeremonyUrl: function (keyCeremony) { const page = this.isAdmin ? RouterService.routes.viewKeyCeremonyAdminPage : RouterService.routes.viewKeyCeremonyGuardianPage; return RouterService.getUrl(page, { keyCeremonyId: keyCeremony.id, }); }, }, template: /*html*/ ` <div v-if="showWhenEmpty && !keyCeremonies.length"> <p>No key ceremonies found.</p> </div> <div v-if="keyCeremonies.length" class="d-grid gap-2 d-md-block"> <h2>Active Key Ceremonies</h2> <a :href="getKeyCeremonyUrl(keyCeremony)" v-for="keyCeremony in keyCeremonies" class="btn btn-primary me-2 mt-2">{{ keyCeremony.key_ceremony_name }}</a> </div> `, }; ================================================ FILE: src/electionguard_gui/web/components/shared/login-component.js ================================================ import AuthorizationService from "../../services/authorization-service.js"; export default { mounted: async function () { const userId = await AuthorizationService.getUserId(); this.userId = userId; }, data() { return { userId: null, }; }, methods: { async createUser() { const form = document.getElementById("mainForm"); if (form.checkValidity()) { await AuthorizationService.setUserId(this.userId); this.$emit("login", this.userId); } form.classList.add("was-validated"); }, }, template: /*html*/ ` <div class="col-md-8 mx-auto"> <form id="mainForm" class="needs-validation" novalidate @submit.prevent="createUser"> <div class="row"> <div class="col-12"> <h1>User Setup</h1> </div> <div class="col-12"> <label for="keyName" class="form-label">User ID</label> <input type="textbox" class="form-control" v-model="userId" required pattern="[a-zA-Z0-9]+" placeholder="Enter your name or other identifier" /> <div class="invalid-feedback"> User ID is required and cannot contain spaces </div> </div> <div class="col-12 mt-4"> <input type="submit" class="btn btn-primary" text="Continue" /> </div> </div> </form> </div> `, }; ================================================ FILE: src/electionguard_gui/web/components/shared/navbar-component.js ================================================ export default { props: { userId: String, }, template: /*html*/ ` <nav class="navbar navbar-expand-md navbar-dark bg-primary"> <div class="container-fluid"> <a class="navbar-brand" href="#/"> <img src="images/electionguard-icon.svg" height="30" class="d-inline-block align-text-top" /> ElectionGuard </a> <button class="navbar-toggler ms-auto me-2" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation" > <span class="navbar-toggler-icon"></span> </button> <div class="navbar-text"> <span class="nav-link">{{userId}}</span> </div> </div> </nav>`, }; ================================================ FILE: src/electionguard_gui/web/components/shared/not-found-component.js ================================================ export default { template: /*html*/ `<h1>Page Not Found</h1>`, }; ================================================ FILE: src/electionguard_gui/web/components/shared/spinner-component.js ================================================ let isMounted = false; /* Prevent duplicated styles in head tag */ export default { props: { visible: { type: Boolean, default: true, }, }, mounted: function () { console.debug("spinner activated"); if (!isMounted) { let styleElem = document.createElement("link"); styleElem.id = "spinner-style"; styleElem.rel = "stylesheet"; styleElem.href = "./css/spinner.css"; document.head.appendChild(styleElem); isMounted = true; } }, unmounted: function () { console.debug("spinner deactivated"); if (isMounted) { document.head.removeChild(document.getElementById("spinner-style")); isMounted = false; } }, template: /*html*/ ` <div class="windows8" v-if="visible"> <div class="wBall" id="wBall_1"> <div class="wInnerBall"></div> </div> <div class="wBall" id="wBall_2"> <div class="wInnerBall"></div> </div> <div class="wBall" id="wBall_3"> <div class="wInnerBall"></div> </div> <div class="wBall" id="wBall_4"> <div class="wInnerBall"></div> </div> <div class="wBall" id="wBall_5"> <div class="wInnerBall"></div> </div> </div> `, }; ================================================ FILE: src/electionguard_gui/web/components/shared/view-plaintext-ballot-component.js ================================================ export default { props: { ballot: Object, }, template: /*html*/ ` <div v-for="contest in ballot" class="mb-5"> <h2>{{contest.name}}</h2> <table class="table table-striped"> <thead> <tr> <th>Choice</th> <th>Party</th> <th class="text-end" width="100">Votes</th> <th class="text-end" width="100">%</th> </tr> </thead> <tbody> <tr v-for="contestInfo in contest.details.selections"> <td>{{contestInfo.name}}</td> <td>{{contestInfo.party}}</td> <td class="text-end">{{contestInfo.tally}}</td> <td class="text-end">{{(contestInfo.percent * 100).toFixed(2) }}%</td> </tr> <tr class="table-secondary"> <td></td> <td></td> <td class="text-end"><strong>{{contest.details.nonWriteInTotal}}</strong></td> <td class="text-end"><strong>100.00%</strong></td> </tr> <tr v-if="contest.details.writeInTotal !== null"> <td></td> <td class="text-end">Write-Ins</td> <td class="text-end">{{contest.details.writeInTotal}}</td> <td class="text-end"></td> </tr> </tbody> </table> </div> `, }; ================================================ FILE: src/electionguard_gui/web/css/bootstrap-icons.css ================================================ /* https://cdn.jsdelivr.net/npm/bootstrap-icons@1.9.1/font/bootstrap-icons.css */ @font-face { font-display: block; font-family: "bootstrap-icons"; src: url("/fonts/bootstrap-icons.woff2?8d200481aa7f02a2d63a331fc782cfaf") format("woff2"), url("/fonts/bootstrap-icons.woff?8d200481aa7f02a2d63a331fc782cfaf") format("woff"); } .bi::before, [class^="bi-"]::before, [class*=" bi-"]::before { display: inline-block; font-family: bootstrap-icons !important; font-style: normal; font-weight: normal !important; font-variant: normal; text-transform: none; line-height: 1; vertical-align: -0.125em; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } .bi-123::before { content: "\f67f"; } .bi-alarm-fill::before { content: "\f101"; } .bi-alarm::before { content: "\f102"; } .bi-align-bottom::before { content: "\f103"; } .bi-align-center::before { content: "\f104"; } .bi-align-end::before { content: "\f105"; } .bi-align-middle::before { content: "\f106"; } .bi-align-start::before { content: "\f107"; } .bi-align-top::before { content: "\f108"; } .bi-alt::before { content: "\f109"; } .bi-app-indicator::before { content: "\f10a"; } .bi-app::before { content: "\f10b"; } .bi-archive-fill::before { content: "\f10c"; } .bi-archive::before { content: "\f10d"; } .bi-arrow-90deg-down::before { content: "\f10e"; } .bi-arrow-90deg-left::before { content: "\f10f"; } .bi-arrow-90deg-right::before { content: "\f110"; } .bi-arrow-90deg-up::before { content: "\f111"; } .bi-arrow-bar-down::before { content: "\f112"; } .bi-arrow-bar-left::before { content: "\f113"; } .bi-arrow-bar-right::before { content: "\f114"; } .bi-arrow-bar-up::before { content: "\f115"; } .bi-arrow-clockwise::before { content: "\f116"; } .bi-arrow-counterclockwise::before { content: "\f117"; } .bi-arrow-down-circle-fill::before { content: "\f118"; } .bi-arrow-down-circle::before { content: "\f119"; } .bi-arrow-down-left-circle-fill::before { content: "\f11a"; } .bi-arrow-down-left-circle::before { content: "\f11b"; } .bi-arrow-down-left-square-fill::before { content: "\f11c"; } .bi-arrow-down-left-square::before { content: "\f11d"; } .bi-arrow-down-left::before { content: "\f11e"; } .bi-arrow-down-right-circle-fill::before { content: "\f11f"; } .bi-arrow-down-right-circle::before { content: "\f120"; } .bi-arrow-down-right-square-fill::before { content: "\f121"; } .bi-arrow-down-right-square::before { content: "\f122"; } .bi-arrow-down-right::before { content: "\f123"; } .bi-arrow-down-short::before { content: "\f124"; } .bi-arrow-down-square-fill::before { content: "\f125"; } .bi-arrow-down-square::before { content: "\f126"; } .bi-arrow-down-up::before { content: "\f127"; } .bi-arrow-down::before { content: "\f128"; } .bi-arrow-left-circle-fill::before { content: "\f129"; } .bi-arrow-left-circle::before { content: "\f12a"; } .bi-arrow-left-right::before { content: "\f12b"; } .bi-arrow-left-short::before { content: "\f12c"; } .bi-arrow-left-square-fill::before { content: "\f12d"; } .bi-arrow-left-square::before { content: "\f12e"; } .bi-arrow-left::before { content: "\f12f"; } .bi-arrow-repeat::before { content: "\f130"; } .bi-arrow-return-left::before { content: "\f131"; } .bi-arrow-return-right::before { content: "\f132"; } .bi-arrow-right-circle-fill::before { content: "\f133"; } .bi-arrow-right-circle::before { content: "\f134"; } .bi-arrow-right-short::before { content: "\f135"; } .bi-arrow-right-square-fill::before { content: "\f136"; } .bi-arrow-right-square::before { content: "\f137"; } .bi-arrow-right::before { content: "\f138"; } .bi-arrow-up-circle-fill::before { content: "\f139"; } .bi-arrow-up-circle::before { content: "\f13a"; } .bi-arrow-up-left-circle-fill::before { content: "\f13b"; } .bi-arrow-up-left-circle::before { content: "\f13c"; } .bi-arrow-up-left-square-fill::before { content: "\f13d"; } .bi-arrow-up-left-square::before { content: "\f13e"; } .bi-arrow-up-left::before { content: "\f13f"; } .bi-arrow-up-right-circle-fill::before { content: "\f140"; } .bi-arrow-up-right-circle::before { content: "\f141"; } .bi-arrow-up-right-square-fill::before { content: "\f142"; } .bi-arrow-up-right-square::before { content: "\f143"; } .bi-arrow-up-right::before { content: "\f144"; } .bi-arrow-up-short::before { content: "\f145"; } .bi-arrow-up-square-fill::before { content: "\f146"; } .bi-arrow-up-square::before { content: "\f147"; } .bi-arrow-up::before { content: "\f148"; } .bi-arrows-angle-contract::before { content: "\f149"; } .bi-arrows-angle-expand::before { content: "\f14a"; } .bi-arrows-collapse::before { content: "\f14b"; } .bi-arrows-expand::before { content: "\f14c"; } .bi-arrows-fullscreen::before { content: "\f14d"; } .bi-arrows-move::before { content: "\f14e"; } .bi-aspect-ratio-fill::before { content: "\f14f"; } .bi-aspect-ratio::before { content: "\f150"; } .bi-asterisk::before { content: "\f151"; } .bi-at::before { content: "\f152"; } .bi-award-fill::before { content: "\f153"; } .bi-award::before { content: "\f154"; } .bi-back::before { content: "\f155"; } .bi-backspace-fill::before { content: "\f156"; } .bi-backspace-reverse-fill::before { content: "\f157"; } .bi-backspace-reverse::before { content: "\f158"; } .bi-backspace::before { content: "\f159"; } .bi-badge-3d-fill::before { content: "\f15a"; } .bi-badge-3d::before { content: "\f15b"; } .bi-badge-4k-fill::before { content: "\f15c"; } .bi-badge-4k::before { content: "\f15d"; } .bi-badge-8k-fill::before { content: "\f15e"; } .bi-badge-8k::before { content: "\f15f"; } .bi-badge-ad-fill::before { content: "\f160"; } .bi-badge-ad::before { content: "\f161"; } .bi-badge-ar-fill::before { content: "\f162"; } .bi-badge-ar::before { content: "\f163"; } .bi-badge-cc-fill::before { content: "\f164"; } .bi-badge-cc::before { content: "\f165"; } .bi-badge-hd-fill::before { content: "\f166"; } .bi-badge-hd::before { content: "\f167"; } .bi-badge-tm-fill::before { content: "\f168"; } .bi-badge-tm::before { content: "\f169"; } .bi-badge-vo-fill::before { content: "\f16a"; } .bi-badge-vo::before { content: "\f16b"; } .bi-badge-vr-fill::before { content: "\f16c"; } .bi-badge-vr::before { content: "\f16d"; } .bi-badge-wc-fill::before { content: "\f16e"; } .bi-badge-wc::before { content: "\f16f"; } .bi-bag-check-fill::before { content: "\f170"; } .bi-bag-check::before { content: "\f171"; } .bi-bag-dash-fill::before { content: "\f172"; } .bi-bag-dash::before { content: "\f173"; } .bi-bag-fill::before { content: "\f174"; } .bi-bag-plus-fill::before { content: "\f175"; } .bi-bag-plus::before { content: "\f176"; } .bi-bag-x-fill::before { content: "\f177"; } .bi-bag-x::before { content: "\f178"; } .bi-bag::before { content: "\f179"; } .bi-bar-chart-fill::before { content: "\f17a"; } .bi-bar-chart-line-fill::before { content: "\f17b"; } .bi-bar-chart-line::before { content: "\f17c"; } .bi-bar-chart-steps::before { content: "\f17d"; } .bi-bar-chart::before { content: "\f17e"; } .bi-basket-fill::before { content: "\f17f"; } .bi-basket::before { content: "\f180"; } .bi-basket2-fill::before { content: "\f181"; } .bi-basket2::before { content: "\f182"; } .bi-basket3-fill::before { content: "\f183"; } .bi-basket3::before { content: "\f184"; } .bi-battery-charging::before { content: "\f185"; } .bi-battery-full::before { content: "\f186"; } .bi-battery-half::before { content: "\f187"; } .bi-battery::before { content: "\f188"; } .bi-bell-fill::before { content: "\f189"; } .bi-bell::before { content: "\f18a"; } .bi-bezier::before { content: "\f18b"; } .bi-bezier2::before { content: "\f18c"; } .bi-bicycle::before { content: "\f18d"; } .bi-binoculars-fill::before { content: "\f18e"; } .bi-binoculars::before { content: "\f18f"; } .bi-blockquote-left::before { content: "\f190"; } .bi-blockquote-right::before { content: "\f191"; } .bi-book-fill::before { content: "\f192"; } .bi-book-half::before { content: "\f193"; } .bi-book::before { content: "\f194"; } .bi-bookmark-check-fill::before { content: "\f195"; } .bi-bookmark-check::before { content: "\f196"; } .bi-bookmark-dash-fill::before { content: "\f197"; } .bi-bookmark-dash::before { content: "\f198"; } .bi-bookmark-fill::before { content: "\f199"; } .bi-bookmark-heart-fill::before { content: "\f19a"; } .bi-bookmark-heart::before { content: "\f19b"; } .bi-bookmark-plus-fill::before { content: "\f19c"; } .bi-bookmark-plus::before { content: "\f19d"; } .bi-bookmark-star-fill::before { content: "\f19e"; } .bi-bookmark-star::before { content: "\f19f"; } .bi-bookmark-x-fill::before { content: "\f1a0"; } .bi-bookmark-x::before { content: "\f1a1"; } .bi-bookmark::before { content: "\f1a2"; } .bi-bookmarks-fill::before { content: "\f1a3"; } .bi-bookmarks::before { content: "\f1a4"; } .bi-bookshelf::before { content: "\f1a5"; } .bi-bootstrap-fill::before { content: "\f1a6"; } .bi-bootstrap-reboot::before { content: "\f1a7"; } .bi-bootstrap::before { content: "\f1a8"; } .bi-border-all::before { content: "\f1a9"; } .bi-border-bottom::before { content: "\f1aa"; } .bi-border-center::before { content: "\f1ab"; } .bi-border-inner::before { content: "\f1ac"; } .bi-border-left::before { content: "\f1ad"; } .bi-border-middle::before { content: "\f1ae"; } .bi-border-outer::before { content: "\f1af"; } .bi-border-right::before { content: "\f1b0"; } .bi-border-style::before { content: "\f1b1"; } .bi-border-top::before { content: "\f1b2"; } .bi-border-width::before { content: "\f1b3"; } .bi-border::before { content: "\f1b4"; } .bi-bounding-box-circles::before { content: "\f1b5"; } .bi-bounding-box::before { content: "\f1b6"; } .bi-box-arrow-down-left::before { content: "\f1b7"; } .bi-box-arrow-down-right::before { content: "\f1b8"; } .bi-box-arrow-down::before { content: "\f1b9"; } .bi-box-arrow-in-down-left::before { content: "\f1ba"; } .bi-box-arrow-in-down-right::before { content: "\f1bb"; } .bi-box-arrow-in-down::before { content: "\f1bc"; } .bi-box-arrow-in-left::before { content: "\f1bd"; } .bi-box-arrow-in-right::before { content: "\f1be"; } .bi-box-arrow-in-up-left::before { content: "\f1bf"; } .bi-box-arrow-in-up-right::before { content: "\f1c0"; } .bi-box-arrow-in-up::before { content: "\f1c1"; } .bi-box-arrow-left::before { content: "\f1c2"; } .bi-box-arrow-right::before { content: "\f1c3"; } .bi-box-arrow-up-left::before { content: "\f1c4"; } .bi-box-arrow-up-right::before { content: "\f1c5"; } .bi-box-arrow-up::before { content: "\f1c6"; } .bi-box-seam::before { content: "\f1c7"; } .bi-box::before { content: "\f1c8"; } .bi-braces::before { content: "\f1c9"; } .bi-bricks::before { content: "\f1ca"; } .bi-briefcase-fill::before { content: "\f1cb"; } .bi-briefcase::before { content: "\f1cc"; } .bi-brightness-alt-high-fill::before { content: "\f1cd"; } .bi-brightness-alt-high::before { content: "\f1ce"; } .bi-brightness-alt-low-fill::before { content: "\f1cf"; } .bi-brightness-alt-low::before { content: "\f1d0"; } .bi-brightness-high-fill::before { content: "\f1d1"; } .bi-brightness-high::before { content: "\f1d2"; } .bi-brightness-low-fill::before { content: "\f1d3"; } .bi-brightness-low::before { content: "\f1d4"; } .bi-broadcast-pin::before { content: "\f1d5"; } .bi-broadcast::before { content: "\f1d6"; } .bi-brush-fill::before { content: "\f1d7"; } .bi-brush::before { content: "\f1d8"; } .bi-bucket-fill::before { content: "\f1d9"; } .bi-bucket::before { content: "\f1da"; } .bi-bug-fill::before { content: "\f1db"; } .bi-bug::before { content: "\f1dc"; } .bi-building::before { content: "\f1dd"; } .bi-bullseye::before { content: "\f1de"; } .bi-calculator-fill::before { content: "\f1df"; } .bi-calculator::before { content: "\f1e0"; } .bi-calendar-check-fill::before { content: "\f1e1"; } .bi-calendar-check::before { content: "\f1e2"; } .bi-calendar-date-fill::before { content: "\f1e3"; } .bi-calendar-date::before { content: "\f1e4"; } .bi-calendar-day-fill::before { content: "\f1e5"; } .bi-calendar-day::before { content: "\f1e6"; } .bi-calendar-event-fill::before { content: "\f1e7"; } .bi-calendar-event::before { content: "\f1e8"; } .bi-calendar-fill::before { content: "\f1e9"; } .bi-calendar-minus-fill::before { content: "\f1ea"; } .bi-calendar-minus::before { content: "\f1eb"; } .bi-calendar-month-fill::before { content: "\f1ec"; } .bi-calendar-month::before { content: "\f1ed"; } .bi-calendar-plus-fill::before { content: "\f1ee"; } .bi-calendar-plus::before { content: "\f1ef"; } .bi-calendar-range-fill::before { content: "\f1f0"; } .bi-calendar-range::before { content: "\f1f1"; } .bi-calendar-week-fill::before { content: "\f1f2"; } .bi-calendar-week::before { content: "\f1f3"; } .bi-calendar-x-fill::before { content: "\f1f4"; } .bi-calendar-x::before { content: "\f1f5"; } .bi-calendar::before { content: "\f1f6"; } .bi-calendar2-check-fill::before { content: "\f1f7"; } .bi-calendar2-check::before { content: "\f1f8"; } .bi-calendar2-date-fill::before { content: "\f1f9"; } .bi-calendar2-date::before { content: "\f1fa"; } .bi-calendar2-day-fill::before { content: "\f1fb"; } .bi-calendar2-day::before { content: "\f1fc"; } .bi-calendar2-event-fill::before { content: "\f1fd"; } .bi-calendar2-event::before { content: "\f1fe"; } .bi-calendar2-fill::before { content: "\f1ff"; } .bi-calendar2-minus-fill::before { content: "\f200"; } .bi-calendar2-minus::before { content: "\f201"; } .bi-calendar2-month-fill::before { content: "\f202"; } .bi-calendar2-month::before { content: "\f203"; } .bi-calendar2-plus-fill::before { content: "\f204"; } .bi-calendar2-plus::before { content: "\f205"; } .bi-calendar2-range-fill::before { content: "\f206"; } .bi-calendar2-range::before { content: "\f207"; } .bi-calendar2-week-fill::before { content: "\f208"; } .bi-calendar2-week::before { content: "\f209"; } .bi-calendar2-x-fill::before { content: "\f20a"; } .bi-calendar2-x::before { content: "\f20b"; } .bi-calendar2::before { content: "\f20c"; } .bi-calendar3-event-fill::before { content: "\f20d"; } .bi-calendar3-event::before { content: "\f20e"; } .bi-calendar3-fill::before { content: "\f20f"; } .bi-calendar3-range-fill::before { content: "\f210"; } .bi-calendar3-range::before { content: "\f211"; } .bi-calendar3-week-fill::before { content: "\f212"; } .bi-calendar3-week::before { content: "\f213"; } .bi-calendar3::before { content: "\f214"; } .bi-calendar4-event::before { content: "\f215"; } .bi-calendar4-range::before { content: "\f216"; } .bi-calendar4-week::before { content: "\f217"; } .bi-calendar4::before { content: "\f218"; } .bi-camera-fill::before { content: "\f219"; } .bi-camera-reels-fill::before { content: "\f21a"; } .bi-camera-reels::before { content: "\f21b"; } .bi-camera-video-fill::before { content: "\f21c"; } .bi-camera-video-off-fill::before { content: "\f21d"; } .bi-camera-video-off::before { content: "\f21e"; } .bi-camera-video::before { content: "\f21f"; } .bi-camera::before { content: "\f220"; } .bi-camera2::before { content: "\f221"; } .bi-capslock-fill::before { content: "\f222"; } .bi-capslock::before { content: "\f223"; } .bi-card-checklist::before { content: "\f224"; } .bi-card-heading::before { content: "\f225"; } .bi-card-image::before { content: "\f226"; } .bi-card-list::before { content: "\f227"; } .bi-card-text::before { content: "\f228"; } .bi-caret-down-fill::before { content: "\f229"; } .bi-caret-down-square-fill::before { content: "\f22a"; } .bi-caret-down-square::before { content: "\f22b"; } .bi-caret-down::before { content: "\f22c"; } .bi-caret-left-fill::before { content: "\f22d"; } .bi-caret-left-square-fill::before { content: "\f22e"; } .bi-caret-left-square::before { content: "\f22f"; } .bi-caret-left::before { content: "\f230"; } .bi-caret-right-fill::before { content: "\f231"; } .bi-caret-right-square-fill::before { content: "\f232"; } .bi-caret-right-square::before { content: "\f233"; } .bi-caret-right::before { content: "\f234"; } .bi-caret-up-fill::before { content: "\f235"; } .bi-caret-up-square-fill::before { content: "\f236"; } .bi-caret-up-square::before { content: "\f237"; } .bi-caret-up::before { content: "\f238"; } .bi-cart-check-fill::before { content: "\f239"; } .bi-cart-check::before { content: "\f23a"; } .bi-cart-dash-fill::before { content: "\f23b"; } .bi-cart-dash::before { content: "\f23c"; } .bi-cart-fill::before { content: "\f23d"; } .bi-cart-plus-fill::before { content: "\f23e"; } .bi-cart-plus::before { content: "\f23f"; } .bi-cart-x-fill::before { content: "\f240"; } .bi-cart-x::before { content: "\f241"; } .bi-cart::before { content: "\f242"; } .bi-cart2::before { content: "\f243"; } .bi-cart3::before { content: "\f244"; } .bi-cart4::before { content: "\f245"; } .bi-cash-stack::before { content: "\f246"; } .bi-cash::before { content: "\f247"; } .bi-cast::before { content: "\f248"; } .bi-chat-dots-fill::before { content: "\f249"; } .bi-chat-dots::before { content: "\f24a"; } .bi-chat-fill::before { content: "\f24b"; } .bi-chat-left-dots-fill::before { content: "\f24c"; } .bi-chat-left-dots::before { content: "\f24d"; } .bi-chat-left-fill::before { content: "\f24e"; } .bi-chat-left-quote-fill::before { content: "\f24f"; } .bi-chat-left-quote::before { content: "\f250"; } .bi-chat-left-text-fill::before { content: "\f251"; } .bi-chat-left-text::before { content: "\f252"; } .bi-chat-left::before { content: "\f253"; } .bi-chat-quote-fill::before { content: "\f254"; } .bi-chat-quote::before { content: "\f255"; } .bi-chat-right-dots-fill::before { content: "\f256"; } .bi-chat-right-dots::before { content: "\f257"; } .bi-chat-right-fill::before { content: "\f258"; } .bi-chat-right-quote-fill::before { content: "\f259"; } .bi-chat-right-quote::before { content: "\f25a"; } .bi-chat-right-text-fill::before { content: "\f25b"; } .bi-chat-right-text::before { content: "\f25c"; } .bi-chat-right::before { content: "\f25d"; } .bi-chat-square-dots-fill::before { content: "\f25e"; } .bi-chat-square-dots::before { content: "\f25f"; } .bi-chat-square-fill::before { content: "\f260"; } .bi-chat-square-quote-fill::before { content: "\f261"; } .bi-chat-square-quote::before { content: "\f262"; } .bi-chat-square-text-fill::before { content: "\f263"; } .bi-chat-square-text::before { content: "\f264"; } .bi-chat-square::before { content: "\f265"; } .bi-chat-text-fill::before { content: "\f266"; } .bi-chat-text::before { content: "\f267"; } .bi-chat::before { content: "\f268"; } .bi-check-all::before { content: "\f269"; } .bi-check-circle-fill::before { content: "\f26a"; } .bi-check-circle::before { content: "\f26b"; } .bi-check-square-fill::before { content: "\f26c"; } .bi-check-square::before { content: "\f26d"; } .bi-check::before { content: "\f26e"; } .bi-check2-all::before { content: "\f26f"; } .bi-check2-circle::before { content: "\f270"; } .bi-check2-square::before { content: "\f271"; } .bi-check2::before { content: "\f272"; } .bi-chevron-bar-contract::before { content: "\f273"; } .bi-chevron-bar-down::before { content: "\f274"; } .bi-chevron-bar-expand::before { content: "\f275"; } .bi-chevron-bar-left::before { content: "\f276"; } .bi-chevron-bar-right::before { content: "\f277"; } .bi-chevron-bar-up::before { content: "\f278"; } .bi-chevron-compact-down::before { content: "\f279"; } .bi-chevron-compact-left::before { content: "\f27a"; } .bi-chevron-compact-right::before { content: "\f27b"; } .bi-chevron-compact-up::before { content: "\f27c"; } .bi-chevron-contract::before { content: "\f27d"; } .bi-chevron-double-down::before { content: "\f27e"; } .bi-chevron-double-left::before { content: "\f27f"; } .bi-chevron-double-right::before { content: "\f280"; } .bi-chevron-double-up::before { content: "\f281"; } .bi-chevron-down::before { content: "\f282"; } .bi-chevron-expand::before { content: "\f283"; } .bi-chevron-left::before { content: "\f284"; } .bi-chevron-right::before { content: "\f285"; } .bi-chevron-up::before { content: "\f286"; } .bi-circle-fill::before { content: "\f287"; } .bi-circle-half::before { content: "\f288"; } .bi-circle-square::before { content: "\f289"; } .bi-circle::before { content: "\f28a"; } .bi-clipboard-check::before { content: "\f28b"; } .bi-clipboard-data::before { content: "\f28c"; } .bi-clipboard-minus::before { content: "\f28d"; } .bi-clipboard-plus::before { content: "\f28e"; } .bi-clipboard-x::before { content: "\f28f"; } .bi-clipboard::before { content: "\f290"; } .bi-clock-fill::before { content: "\f291"; } .bi-clock-history::before { content: "\f292"; } .bi-clock::before { content: "\f293"; } .bi-cloud-arrow-down-fill::before { content: "\f294"; } .bi-cloud-arrow-down::before { content: "\f295"; } .bi-cloud-arrow-up-fill::before { content: "\f296"; } .bi-cloud-arrow-up::before { content: "\f297"; } .bi-cloud-check-fill::before { content: "\f298"; } .bi-cloud-check::before { content: "\f299"; } .bi-cloud-download-fill::before { content: "\f29a"; } .bi-cloud-download::before { content: "\f29b"; } .bi-cloud-drizzle-fill::before { content: "\f29c"; } .bi-cloud-drizzle::before { content: "\f29d"; } .bi-cloud-fill::before { content: "\f29e"; } .bi-cloud-fog-fill::before { content: "\f29f"; } .bi-cloud-fog::before { content: "\f2a0"; } .bi-cloud-fog2-fill::before { content: "\f2a1"; } .bi-cloud-fog2::before { content: "\f2a2"; } .bi-cloud-hail-fill::before { content: "\f2a3"; } .bi-cloud-hail::before { content: "\f2a4"; } .bi-cloud-haze-1::before { content: "\f2a5"; } .bi-cloud-haze-fill::before { content: "\f2a6"; } .bi-cloud-haze::before { content: "\f2a7"; } .bi-cloud-haze2-fill::before { content: "\f2a8"; } .bi-cloud-lightning-fill::before { content: "\f2a9"; } .bi-cloud-lightning-rain-fill::before { content: "\f2aa"; } .bi-cloud-lightning-rain::before { content: "\f2ab"; } .bi-cloud-lightning::before { content: "\f2ac"; } .bi-cloud-minus-fill::before { content: "\f2ad"; } .bi-cloud-minus::before { content: "\f2ae"; } .bi-cloud-moon-fill::before { content: "\f2af"; } .bi-cloud-moon::before { content: "\f2b0"; } .bi-cloud-plus-fill::before { content: "\f2b1"; } .bi-cloud-plus::before { content: "\f2b2"; } .bi-cloud-rain-fill::before { content: "\f2b3"; } .bi-cloud-rain-heavy-fill::before { content: "\f2b4"; } .bi-cloud-rain-heavy::before { content: "\f2b5"; } .bi-cloud-rain::before { content: "\f2b6"; } .bi-cloud-slash-fill::before { content: "\f2b7"; } .bi-cloud-slash::before { content: "\f2b8"; } .bi-cloud-sleet-fill::before { content: "\f2b9"; } .bi-cloud-sleet::before { content: "\f2ba"; } .bi-cloud-snow-fill::before { content: "\f2bb"; } .bi-cloud-snow::before { content: "\f2bc"; } .bi-cloud-sun-fill::before { content: "\f2bd"; } .bi-cloud-sun::before { content: "\f2be"; } .bi-cloud-upload-fill::before { content: "\f2bf"; } .bi-cloud-upload::before { content: "\f2c0"; } .bi-cloud::before { content: "\f2c1"; } .bi-clouds-fill::before { content: "\f2c2"; } .bi-clouds::before { content: "\f2c3"; } .bi-cloudy-fill::before { content: "\f2c4"; } .bi-cloudy::before { content: "\f2c5"; } .bi-code-slash::before { content: "\f2c6"; } .bi-code-square::before { content: "\f2c7"; } .bi-code::before { content: "\f2c8"; } .bi-collection-fill::before { content: "\f2c9"; } .bi-collection-play-fill::before { content: "\f2ca"; } .bi-collection-play::before { content: "\f2cb"; } .bi-collection::before { content: "\f2cc"; } .bi-columns-gap::before { content: "\f2cd"; } .bi-columns::before { content: "\f2ce"; } .bi-command::before { content: "\f2cf"; } .bi-compass-fill::before { content: "\f2d0"; } .bi-compass::before { content: "\f2d1"; } .bi-cone-striped::before { content: "\f2d2"; } .bi-cone::before { content: "\f2d3"; } .bi-controller::before { content: "\f2d4"; } .bi-cpu-fill::before { content: "\f2d5"; } .bi-cpu::before { content: "\f2d6"; } .bi-credit-card-2-back-fill::before { content: "\f2d7"; } .bi-credit-card-2-back::before { content: "\f2d8"; } .bi-credit-card-2-front-fill::before { content: "\f2d9"; } .bi-credit-card-2-front::before { content: "\f2da"; } .bi-credit-card-fill::before { content: "\f2db"; } .bi-credit-card::before { content: "\f2dc"; } .bi-crop::before { content: "\f2dd"; } .bi-cup-fill::before { content: "\f2de"; } .bi-cup-straw::before { content: "\f2df"; } .bi-cup::before { content: "\f2e0"; } .bi-cursor-fill::before { content: "\f2e1"; } .bi-cursor-text::before { content: "\f2e2"; } .bi-cursor::before { content: "\f2e3"; } .bi-dash-circle-dotted::before { content: "\f2e4"; } .bi-dash-circle-fill::before { content: "\f2e5"; } .bi-dash-circle::before { content: "\f2e6"; } .bi-dash-square-dotted::before { content: "\f2e7"; } .bi-dash-square-fill::before { content: "\f2e8"; } .bi-dash-square::before { content: "\f2e9"; } .bi-dash::before { content: "\f2ea"; } .bi-diagram-2-fill::before { content: "\f2eb"; } .bi-diagram-2::before { content: "\f2ec"; } .bi-diagram-3-fill::before { content: "\f2ed"; } .bi-diagram-3::before { content: "\f2ee"; } .bi-diamond-fill::before { content: "\f2ef"; } .bi-diamond-half::before { content: "\f2f0"; } .bi-diamond::before { content: "\f2f1"; } .bi-dice-1-fill::before { content: "\f2f2"; } .bi-dice-1::before { content: "\f2f3"; } .bi-dice-2-fill::before { content: "\f2f4"; } .bi-dice-2::before { content: "\f2f5"; } .bi-dice-3-fill::before { content: "\f2f6"; } .bi-dice-3::before { content: "\f2f7"; } .bi-dice-4-fill::before { content: "\f2f8"; } .bi-dice-4::before { content: "\f2f9"; } .bi-dice-5-fill::before { content: "\f2fa"; } .bi-dice-5::before { content: "\f2fb"; } .bi-dice-6-fill::before { content: "\f2fc"; } .bi-dice-6::before { content: "\f2fd"; } .bi-disc-fill::before { content: "\f2fe"; } .bi-disc::before { content: "\f2ff"; } .bi-discord::before { content: "\f300"; } .bi-display-fill::before { content: "\f301"; } .bi-display::before { content: "\f302"; } .bi-distribute-horizontal::before { content: "\f303"; } .bi-distribute-vertical::before { content: "\f304"; } .bi-door-closed-fill::before { content: "\f305"; } .bi-door-closed::before { content: "\f306"; } .bi-door-open-fill::before { content: "\f307"; } .bi-door-open::before { content: "\f308"; } .bi-dot::before { content: "\f309"; } .bi-download::before { content: "\f30a"; } .bi-droplet-fill::before { content: "\f30b"; } .bi-droplet-half::before { content: "\f30c"; } .bi-droplet::before { content: "\f30d"; } .bi-earbuds::before { content: "\f30e"; } .bi-easel-fill::before { content: "\f30f"; } .bi-easel::before { content: "\f310"; } .bi-egg-fill::before { content: "\f311"; } .bi-egg-fried::before { content: "\f312"; } .bi-egg::before { content: "\f313"; } .bi-eject-fill::before { content: "\f314"; } .bi-eject::before { content: "\f315"; } .bi-emoji-angry-fill::before { content: "\f316"; } .bi-emoji-angry::before { content: "\f317"; } .bi-emoji-dizzy-fill::before { content: "\f318"; } .bi-emoji-dizzy::before { content: "\f319"; } .bi-emoji-expressionless-fill::before { content: "\f31a"; } .bi-emoji-expressionless::before { content: "\f31b"; } .bi-emoji-frown-fill::before { content: "\f31c"; } .bi-emoji-frown::before { content: "\f31d"; } .bi-emoji-heart-eyes-fill::before { content: "\f31e"; } .bi-emoji-heart-eyes::before { content: "\f31f"; } .bi-emoji-laughing-fill::before { content: "\f320"; } .bi-emoji-laughing::before { content: "\f321"; } .bi-emoji-neutral-fill::before { content: "\f322"; } .bi-emoji-neutral::before { content: "\f323"; } .bi-emoji-smile-fill::before { content: "\f324"; } .bi-emoji-smile-upside-down-fill::before { content: "\f325"; } .bi-emoji-smile-upside-down::before { content: "\f326"; } .bi-emoji-smile::before { content: "\f327"; } .bi-emoji-sunglasses-fill::before { content: "\f328"; } .bi-emoji-sunglasses::before { content: "\f329"; } .bi-emoji-wink-fill::before { content: "\f32a"; } .bi-emoji-wink::before { content: "\f32b"; } .bi-envelope-fill::before { content: "\f32c"; } .bi-envelope-open-fill::before { content: "\f32d"; } .bi-envelope-open::before { content: "\f32e"; } .bi-envelope::before { content: "\f32f"; } .bi-eraser-fill::before { content: "\f330"; } .bi-eraser::before { content: "\f331"; } .bi-exclamation-circle-fill::before { content: "\f332"; } .bi-exclamation-circle::before { content: "\f333"; } .bi-exclamation-diamond-fill::before { content: "\f334"; } .bi-exclamation-diamond::before { content: "\f335"; } .bi-exclamation-octagon-fill::before { content: "\f336"; } .bi-exclamation-octagon::before { content: "\f337"; } .bi-exclamation-square-fill::before { content: "\f338"; } .bi-exclamation-square::before { content: "\f339"; } .bi-exclamation-triangle-fill::before { content: "\f33a"; } .bi-exclamation-triangle::before { content: "\f33b"; } .bi-exclamation::before { content: "\f33c"; } .bi-exclude::before { content: "\f33d"; } .bi-eye-fill::before { content: "\f33e"; } .bi-eye-slash-fill::before { content: "\f33f"; } .bi-eye-slash::before { content: "\f340"; } .bi-eye::before { content: "\f341"; } .bi-eyedropper::before { content: "\f342"; } .bi-eyeglasses::before { content: "\f343"; } .bi-facebook::before { content: "\f344"; } .bi-file-arrow-down-fill::before { content: "\f345"; } .bi-file-arrow-down::before { content: "\f346"; } .bi-file-arrow-up-fill::before { content: "\f347"; } .bi-file-arrow-up::before { content: "\f348"; } .bi-file-bar-graph-fill::before { content: "\f349"; } .bi-file-bar-graph::before { content: "\f34a"; } .bi-file-binary-fill::before { content: "\f34b"; } .bi-file-binary::before { content: "\f34c"; } .bi-file-break-fill::before { content: "\f34d"; } .bi-file-break::before { content: "\f34e"; } .bi-file-check-fill::before { content: "\f34f"; } .bi-file-check::before { content: "\f350"; } .bi-file-code-fill::before { content: "\f351"; } .bi-file-code::before { content: "\f352"; } .bi-file-diff-fill::before { content: "\f353"; } .bi-file-diff::before { content: "\f354"; } .bi-file-earmark-arrow-down-fill::before { content: "\f355"; } .bi-file-earmark-arrow-down::before { content: "\f356"; } .bi-file-earmark-arrow-up-fill::before { content: "\f357"; } .bi-file-earmark-arrow-up::before { content: "\f358"; } .bi-file-earmark-bar-graph-fill::before { content: "\f359"; } .bi-file-earmark-bar-graph::before { content: "\f35a"; } .bi-file-earmark-binary-fill::before { content: "\f35b"; } .bi-file-earmark-binary::before { content: "\f35c"; } .bi-file-earmark-break-fill::before { content: "\f35d"; } .bi-file-earmark-break::before { content: "\f35e"; } .bi-file-earmark-check-fill::before { content: "\f35f"; } .bi-file-earmark-check::before { content: "\f360"; } .bi-file-earmark-code-fill::before { content: "\f361"; } .bi-file-earmark-code::before { content: "\f362"; } .bi-file-earmark-diff-fill::before { content: "\f363"; } .bi-file-earmark-diff::before { content: "\f364"; } .bi-file-earmark-easel-fill::before { content: "\f365"; } .bi-file-earmark-easel::before { content: "\f366"; } .bi-file-earmark-excel-fill::before { content: "\f367"; } .bi-file-earmark-excel::before { content: "\f368"; } .bi-file-earmark-fill::before { content: "\f369"; } .bi-file-earmark-font-fill::before { content: "\f36a"; } .bi-file-earmark-font::before { content: "\f36b"; } .bi-file-earmark-image-fill::before { content: "\f36c"; } .bi-file-earmark-image::before { content: "\f36d"; } .bi-file-earmark-lock-fill::before { content: "\f36e"; } .bi-file-earmark-lock::before { content: "\f36f"; } .bi-file-earmark-lock2-fill::before { content: "\f370"; } .bi-file-earmark-lock2::before { content: "\f371"; } .bi-file-earmark-medical-fill::before { content: "\f372"; } .bi-file-earmark-medical::before { content: "\f373"; } .bi-file-earmark-minus-fill::before { content: "\f374"; } .bi-file-earmark-minus::before { content: "\f375"; } .bi-file-earmark-music-fill::before { content: "\f376"; } .bi-file-earmark-music::before { content: "\f377"; } .bi-file-earmark-person-fill::before { content: "\f378"; } .bi-file-earmark-person::before { content: "\f379"; } .bi-file-earmark-play-fill::before { content: "\f37a"; } .bi-file-earmark-play::before { content: "\f37b"; } .bi-file-earmark-plus-fill::before { content: "\f37c"; } .bi-file-earmark-plus::before { content: "\f37d"; } .bi-file-earmark-post-fill::before { content: "\f37e"; } .bi-file-earmark-post::before { content: "\f37f"; } .bi-file-earmark-ppt-fill::before { content: "\f380"; } .bi-file-earmark-ppt::before { content: "\f381"; } .bi-file-earmark-richtext-fill::before { content: "\f382"; } .bi-file-earmark-richtext::before { content: "\f383"; } .bi-file-earmark-ruled-fill::before { content: "\f384"; } .bi-file-earmark-ruled::before { content: "\f385"; } .bi-file-earmark-slides-fill::before { content: "\f386"; } .bi-file-earmark-slides::before { content: "\f387"; } .bi-file-earmark-spreadsheet-fill::before { content: "\f388"; } .bi-file-earmark-spreadsheet::before { content: "\f389"; } .bi-file-earmark-text-fill::before { content: "\f38a"; } .bi-file-earmark-text::before { content: "\f38b"; } .bi-file-earmark-word-fill::before { content: "\f38c"; } .bi-file-earmark-word::before { content: "\f38d"; } .bi-file-earmark-x-fill::before { content: "\f38e"; } .bi-file-earmark-x::before { content: "\f38f"; } .bi-file-earmark-zip-fill::before { content: "\f390"; } .bi-file-earmark-zip::before { content: "\f391"; } .bi-file-earmark::before { content: "\f392"; } .bi-file-easel-fill::before { content: "\f393"; } .bi-file-easel::before { content: "\f394"; } .bi-file-excel-fill::before { content: "\f395"; } .bi-file-excel::before { content: "\f396"; } .bi-file-fill::before { content: "\f397"; } .bi-file-font-fill::before { content: "\f398"; } .bi-file-font::before { content: "\f399"; } .bi-file-image-fill::before { content: "\f39a"; } .bi-file-image::before { content: "\f39b"; } .bi-file-lock-fill::before { content: "\f39c"; } .bi-file-lock::before { content: "\f39d"; } .bi-file-lock2-fill::before { content: "\f39e"; } .bi-file-lock2::before { content: "\f39f"; } .bi-file-medical-fill::before { content: "\f3a0"; } .bi-file-medical::before { content: "\f3a1"; } .bi-file-minus-fill::before { content: "\f3a2"; } .bi-file-minus::before { content: "\f3a3"; } .bi-file-music-fill::before { content: "\f3a4"; } .bi-file-music::before { content: "\f3a5"; } .bi-file-person-fill::before { content: "\f3a6"; } .bi-file-person::before { content: "\f3a7"; } .bi-file-play-fill::before { content: "\f3a8"; } .bi-file-play::before { content: "\f3a9"; } .bi-file-plus-fill::before { content: "\f3aa"; } .bi-file-plus::before { content: "\f3ab"; } .bi-file-post-fill::before { content: "\f3ac"; } .bi-file-post::before { content: "\f3ad"; } .bi-file-ppt-fill::before { content: "\f3ae"; } .bi-file-ppt::before { content: "\f3af"; } .bi-file-richtext-fill::before { content: "\f3b0"; } .bi-file-richtext::before { content: "\f3b1"; } .bi-file-ruled-fill::before { content: "\f3b2"; } .bi-file-ruled::before { content: "\f3b3"; } .bi-file-slides-fill::before { content: "\f3b4"; } .bi-file-slides::before { content: "\f3b5"; } .bi-file-spreadsheet-fill::before { content: "\f3b6"; } .bi-file-spreadsheet::before { content: "\f3b7"; } .bi-file-text-fill::before { content: "\f3b8"; } .bi-file-text::before { content: "\f3b9"; } .bi-file-word-fill::before { content: "\f3ba"; } .bi-file-word::before { content: "\f3bb"; } .bi-file-x-fill::before { content: "\f3bc"; } .bi-file-x::before { content: "\f3bd"; } .bi-file-zip-fill::before { content: "\f3be"; } .bi-file-zip::before { content: "\f3bf"; } .bi-file::before { content: "\f3c0"; } .bi-files-alt::before { content: "\f3c1"; } .bi-files::before { content: "\f3c2"; } .bi-film::before { content: "\f3c3"; } .bi-filter-circle-fill::before { content: "\f3c4"; } .bi-filter-circle::before { content: "\f3c5"; } .bi-filter-left::before { content: "\f3c6"; } .bi-filter-right::before { content: "\f3c7"; } .bi-filter-square-fill::before { content: "\f3c8"; } .bi-filter-square::before { content: "\f3c9"; } .bi-filter::before { content: "\f3ca"; } .bi-flag-fill::before { content: "\f3cb"; } .bi-flag::before { content: "\f3cc"; } .bi-flower1::before { content: "\f3cd"; } .bi-flower2::before { content: "\f3ce"; } .bi-flower3::before { content: "\f3cf"; } .bi-folder-check::before { content: "\f3d0"; } .bi-folder-fill::before { content: "\f3d1"; } .bi-folder-minus::before { content: "\f3d2"; } .bi-folder-plus::before { content: "\f3d3"; } .bi-folder-symlink-fill::before { content: "\f3d4"; } .bi-folder-symlink::before { content: "\f3d5"; } .bi-folder-x::before { content: "\f3d6"; } .bi-folder::before { content: "\f3d7"; } .bi-folder2-open::before { content: "\f3d8"; } .bi-folder2::before { content: "\f3d9"; } .bi-fonts::before { content: "\f3da"; } .bi-forward-fill::before { content: "\f3db"; } .bi-forward::before { content: "\f3dc"; } .bi-front::before { content: "\f3dd"; } .bi-fullscreen-exit::before { content: "\f3de"; } .bi-fullscreen::before { content: "\f3df"; } .bi-funnel-fill::before { content: "\f3e0"; } .bi-funnel::before { content: "\f3e1"; } .bi-gear-fill::before { content: "\f3e2"; } .bi-gear-wide-connected::before { content: "\f3e3"; } .bi-gear-wide::before { content: "\f3e4"; } .bi-gear::before { content: "\f3e5"; } .bi-gem::before { content: "\f3e6"; } .bi-geo-alt-fill::before { content: "\f3e7"; } .bi-geo-alt::before { content: "\f3e8"; } .bi-geo-fill::before { content: "\f3e9"; } .bi-geo::before { content: "\f3ea"; } .bi-gift-fill::before { content: "\f3eb"; } .bi-gift::before { content: "\f3ec"; } .bi-github::before { content: "\f3ed"; } .bi-globe::before { content: "\f3ee"; } .bi-globe2::before { content: "\f3ef"; } .bi-google::before { content: "\f3f0"; } .bi-graph-down::before { content: "\f3f1"; } .bi-graph-up::before { content: "\f3f2"; } .bi-grid-1x2-fill::before { content: "\f3f3"; } .bi-grid-1x2::before { content: "\f3f4"; } .bi-grid-3x2-gap-fill::before { content: "\f3f5"; } .bi-grid-3x2-gap::before { content: "\f3f6"; } .bi-grid-3x2::before { content: "\f3f7"; } .bi-grid-3x3-gap-fill::before { content: "\f3f8"; } .bi-grid-3x3-gap::before { content: "\f3f9"; } .bi-grid-3x3::before { content: "\f3fa"; } .bi-grid-fill::before { content: "\f3fb"; } .bi-grid::before { content: "\f3fc"; } .bi-grip-horizontal::before { content: "\f3fd"; } .bi-grip-vertical::before { content: "\f3fe"; } .bi-hammer::before { content: "\f3ff"; } .bi-hand-index-fill::before { content: "\f400"; } .bi-hand-index-thumb-fill::before { content: "\f401"; } .bi-hand-index-thumb::before { content: "\f402"; } .bi-hand-index::before { content: "\f403"; } .bi-hand-thumbs-down-fill::before { content: "\f404"; } .bi-hand-thumbs-down::before { content: "\f405"; } .bi-hand-thumbs-up-fill::before { content: "\f406"; } .bi-hand-thumbs-up::before { content: "\f407"; } .bi-handbag-fill::before { content: "\f408"; } .bi-handbag::before { content: "\f409"; } .bi-hash::before { content: "\f40a"; } .bi-hdd-fill::before { content: "\f40b"; } .bi-hdd-network-fill::before { content: "\f40c"; } .bi-hdd-network::before { content: "\f40d"; } .bi-hdd-rack-fill::before { content: "\f40e"; } .bi-hdd-rack::before { content: "\f40f"; } .bi-hdd-stack-fill::before { content: "\f410"; } .bi-hdd-stack::before { content: "\f411"; } .bi-hdd::before { content: "\f412"; } .bi-headphones::before { content: "\f413"; } .bi-headset::before { content: "\f414"; } .bi-heart-fill::before { content: "\f415"; } .bi-heart-half::before { content: "\f416"; } .bi-heart::before { content: "\f417"; } .bi-heptagon-fill::before { content: "\f418"; } .bi-heptagon-half::before { content: "\f419"; } .bi-heptagon::before { content: "\f41a"; } .bi-hexagon-fill::before { content: "\f41b"; } .bi-hexagon-half::before { content: "\f41c"; } .bi-hexagon::before { content: "\f41d"; } .bi-hourglass-bottom::before { content: "\f41e"; } .bi-hourglass-split::before { content: "\f41f"; } .bi-hourglass-top::before { content: "\f420"; } .bi-hourglass::before { content: "\f421"; } .bi-house-door-fill::before { content: "\f422"; } .bi-house-door::before { content: "\f423"; } .bi-house-fill::before { content: "\f424"; } .bi-house::before { content: "\f425"; } .bi-hr::before { content: "\f426"; } .bi-hurricane::before { content: "\f427"; } .bi-image-alt::before { content: "\f428"; } .bi-image-fill::before { content: "\f429"; } .bi-image::before { content: "\f42a"; } .bi-images::before { content: "\f42b"; } .bi-inbox-fill::before { content: "\f42c"; } .bi-inbox::before { content: "\f42d"; } .bi-inboxes-fill::before { content: "\f42e"; } .bi-inboxes::before { content: "\f42f"; } .bi-info-circle-fill::before { content: "\f430"; } .bi-info-circle::before { content: "\f431"; } .bi-info-square-fill::before { content: "\f432"; } .bi-info-square::before { content: "\f433"; } .bi-info::before { content: "\f434"; } .bi-input-cursor-text::before { content: "\f435"; } .bi-input-cursor::before { content: "\f436"; } .bi-instagram::before { content: "\f437"; } .bi-intersect::before { content: "\f438"; } .bi-journal-album::before { content: "\f439"; } .bi-journal-arrow-down::before { content: "\f43a"; } .bi-journal-arrow-up::before { content: "\f43b"; } .bi-journal-bookmark-fill::before { content: "\f43c"; } .bi-journal-bookmark::before { content: "\f43d"; } .bi-journal-check::before { content: "\f43e"; } .bi-journal-code::before { content: "\f43f"; } .bi-journal-medical::before { content: "\f440"; } .bi-journal-minus::before { content: "\f441"; } .bi-journal-plus::before { content: "\f442"; } .bi-journal-richtext::before { content: "\f443"; } .bi-journal-text::before { content: "\f444"; } .bi-journal-x::before { content: "\f445"; } .bi-journal::before { content: "\f446"; } .bi-journals::before { content: "\f447"; } .bi-joystick::before { content: "\f448"; } .bi-justify-left::before { content: "\f449"; } .bi-justify-right::before { content: "\f44a"; } .bi-justify::before { content: "\f44b"; } .bi-kanban-fill::before { content: "\f44c"; } .bi-kanban::before { content: "\f44d"; } .bi-key-fill::before { content: "\f44e"; } .bi-key::before { content: "\f44f"; } .bi-keyboard-fill::before { content: "\f450"; } .bi-keyboard::before { content: "\f451"; } .bi-ladder::before { content: "\f452"; } .bi-lamp-fill::before { content: "\f453"; } .bi-lamp::before { content: "\f454"; } .bi-laptop-fill::before { content: "\f455"; } .bi-laptop::before { content: "\f456"; } .bi-layer-backward::before { content: "\f457"; } .bi-layer-forward::before { content: "\f458"; } .bi-layers-fill::before { content: "\f459"; } .bi-layers-half::before { content: "\f45a"; } .bi-layers::before { content: "\f45b"; } .bi-layout-sidebar-inset-reverse::before { content: "\f45c"; } .bi-layout-sidebar-inset::before { content: "\f45d"; } .bi-layout-sidebar-reverse::before { content: "\f45e"; } .bi-layout-sidebar::before { content: "\f45f"; } .bi-layout-split::before { content: "\f460"; } .bi-layout-text-sidebar-reverse::before { content: "\f461"; } .bi-layout-text-sidebar::before { content: "\f462"; } .bi-layout-text-window-reverse::before { content: "\f463"; } .bi-layout-text-window::before { content: "\f464"; } .bi-layout-three-columns::before { content: "\f465"; } .bi-layout-wtf::before { content: "\f466"; } .bi-life-preserver::before { content: "\f467"; } .bi-lightbulb-fill::before { content: "\f468"; } .bi-lightbulb-off-fill::before { content: "\f469"; } .bi-lightbulb-off::before { content: "\f46a"; } .bi-lightbulb::before { content: "\f46b"; } .bi-lightning-charge-fill::before { content: "\f46c"; } .bi-lightning-charge::before { content: "\f46d"; } .bi-lightning-fill::before { content: "\f46e"; } .bi-lightning::before { content: "\f46f"; } .bi-link-45deg::before { content: "\f470"; } .bi-link::before { content: "\f471"; } .bi-linkedin::before { content: "\f472"; } .bi-list-check::before { content: "\f473"; } .bi-list-nested::before { content: "\f474"; } .bi-list-ol::before { content: "\f475"; } .bi-list-stars::before { content: "\f476"; } .bi-list-task::before { content: "\f477"; } .bi-list-ul::before { content: "\f478"; } .bi-list::before { content: "\f479"; } .bi-lock-fill::before { content: "\f47a"; } .bi-lock::before { content: "\f47b"; } .bi-mailbox::before { content: "\f47c"; } .bi-mailbox2::before { content: "\f47d"; } .bi-map-fill::before { content: "\f47e"; } .bi-map::before { content: "\f47f"; } .bi-markdown-fill::before { content: "\f480"; } .bi-markdown::before { content: "\f481"; } .bi-mask::before { content: "\f482"; } .bi-megaphone-fill::before { content: "\f483"; } .bi-megaphone::before { content: "\f484"; } .bi-menu-app-fill::before { content: "\f485"; } .bi-menu-app::before { content: "\f486"; } .bi-menu-button-fill::before { content: "\f487"; } .bi-menu-button-wide-fill::before { content: "\f488"; } .bi-menu-button-wide::before { content: "\f489"; } .bi-menu-button::before { content: "\f48a"; } .bi-menu-down::before { content: "\f48b"; } .bi-menu-up::before { content: "\f48c"; } .bi-mic-fill::before { content: "\f48d"; } .bi-mic-mute-fill::before { content: "\f48e"; } .bi-mic-mute::before { content: "\f48f"; } .bi-mic::before { content: "\f490"; } .bi-minecart-loaded::before { content: "\f491"; } .bi-minecart::before { content: "\f492"; } .bi-moisture::before { content: "\f493"; } .bi-moon-fill::before { content: "\f494"; } .bi-moon-stars-fill::before { content: "\f495"; } .bi-moon-stars::before { content: "\f496"; } .bi-moon::before { content: "\f497"; } .bi-mouse-fill::before { content: "\f498"; } .bi-mouse::before { content: "\f499"; } .bi-mouse2-fill::before { content: "\f49a"; } .bi-mouse2::before { content: "\f49b"; } .bi-mouse3-fill::before { content: "\f49c"; } .bi-mouse3::before { content: "\f49d"; } .bi-music-note-beamed::before { content: "\f49e"; } .bi-music-note-list::before { content: "\f49f"; } .bi-music-note::before { content: "\f4a0"; } .bi-music-player-fill::before { content: "\f4a1"; } .bi-music-player::before { content: "\f4a2"; } .bi-newspaper::before { content: "\f4a3"; } .bi-node-minus-fill::before { content: "\f4a4"; } .bi-node-minus::before { content: "\f4a5"; } .bi-node-plus-fill::before { content: "\f4a6"; } .bi-node-plus::before { content: "\f4a7"; } .bi-nut-fill::before { content: "\f4a8"; } .bi-nut::before { content: "\f4a9"; } .bi-octagon-fill::before { content: "\f4aa"; } .bi-octagon-half::before { content: "\f4ab"; } .bi-octagon::before { content: "\f4ac"; } .bi-option::before { content: "\f4ad"; } .bi-outlet::before { content: "\f4ae"; } .bi-paint-bucket::before { content: "\f4af"; } .bi-palette-fill::before { content: "\f4b0"; } .bi-palette::before { content: "\f4b1"; } .bi-palette2::before { content: "\f4b2"; } .bi-paperclip::before { content: "\f4b3"; } .bi-paragraph::before { content: "\f4b4"; } .bi-patch-check-fill::before { content: "\f4b5"; } .bi-patch-check::before { content: "\f4b6"; } .bi-patch-exclamation-fill::before { content: "\f4b7"; } .bi-patch-exclamation::before { content: "\f4b8"; } .bi-patch-minus-fill::before { content: "\f4b9"; } .bi-patch-minus::before { content: "\f4ba"; } .bi-patch-plus-fill::before { content: "\f4bb"; } .bi-patch-plus::before { content: "\f4bc"; } .bi-patch-question-fill::before { content: "\f4bd"; } .bi-patch-question::before { content: "\f4be"; } .bi-pause-btn-fill::before { content: "\f4bf"; } .bi-pause-btn::before { content: "\f4c0"; } .bi-pause-circle-fill::before { content: "\f4c1"; } .bi-pause-circle::before { content: "\f4c2"; } .bi-pause-fill::before { content: "\f4c3"; } .bi-pause::before { content: "\f4c4"; } .bi-peace-fill::before { content: "\f4c5"; } .bi-peace::before { content: "\f4c6"; } .bi-pen-fill::before { content: "\f4c7"; } .bi-pen::before { content: "\f4c8"; } .bi-pencil-fill::before { content: "\f4c9"; } .bi-pencil-square::before { content: "\f4ca"; } .bi-pencil::before { content: "\f4cb"; } .bi-pentagon-fill::before { content: "\f4cc"; } .bi-pentagon-half::before { content: "\f4cd"; } .bi-pentagon::before { content: "\f4ce"; } .bi-people-fill::before { content: "\f4cf"; } .bi-people::before { content: "\f4d0"; } .bi-percent::before { content: "\f4d1"; } .bi-person-badge-fill::before { content: "\f4d2"; } .bi-person-badge::before { content: "\f4d3"; } .bi-person-bounding-box::before { content: "\f4d4"; } .bi-person-check-fill::before { content: "\f4d5"; } .bi-person-check::before { content: "\f4d6"; } .bi-person-circle::before { content: "\f4d7"; } .bi-person-dash-fill::before { content: "\f4d8"; } .bi-person-dash::before { content: "\f4d9"; } .bi-person-fill::before { content: "\f4da"; } .bi-person-lines-fill::before { content: "\f4db"; } .bi-person-plus-fill::before { content: "\f4dc"; } .bi-person-plus::before { content: "\f4dd"; } .bi-person-square::before { content: "\f4de"; } .bi-person-x-fill::before { content: "\f4df"; } .bi-person-x::before { content: "\f4e0"; } .bi-person::before { content: "\f4e1"; } .bi-phone-fill::before { content: "\f4e2"; } .bi-phone-landscape-fill::before { content: "\f4e3"; } .bi-phone-landscape::before { content: "\f4e4"; } .bi-phone-vibrate-fill::before { content: "\f4e5"; } .bi-phone-vibrate::before { content: "\f4e6"; } .bi-phone::before { content: "\f4e7"; } .bi-pie-chart-fill::before { content: "\f4e8"; } .bi-pie-chart::before { content: "\f4e9"; } .bi-pin-angle-fill::before { content: "\f4ea"; } .bi-pin-angle::before { content: "\f4eb"; } .bi-pin-fill::before { content: "\f4ec"; } .bi-pin::before { content: "\f4ed"; } .bi-pip-fill::before { content: "\f4ee"; } .bi-pip::before { content: "\f4ef"; } .bi-play-btn-fill::before { content: "\f4f0"; } .bi-play-btn::before { content: "\f4f1"; } .bi-play-circle-fill::before { content: "\f4f2"; } .bi-play-circle::before { content: "\f4f3"; } .bi-play-fill::before { content: "\f4f4"; } .bi-play::before { content: "\f4f5"; } .bi-plug-fill::before { content: "\f4f6"; } .bi-plug::before { content: "\f4f7"; } .bi-plus-circle-dotted::before { content: "\f4f8"; } .bi-plus-circle-fill::before { content: "\f4f9"; } .bi-plus-circle::before { content: "\f4fa"; } .bi-plus-square-dotted::before { content: "\f4fb"; } .bi-plus-square-fill::before { content: "\f4fc"; } .bi-plus-square::before { content: "\f4fd"; } .bi-plus::before { content: "\f4fe"; } .bi-power::before { content: "\f4ff"; } .bi-printer-fill::before { content: "\f500"; } .bi-printer::before { content: "\f501"; } .bi-puzzle-fill::before { content: "\f502"; } .bi-puzzle::before { content: "\f503"; } .bi-question-circle-fill::before { content: "\f504"; } .bi-question-circle::before { content: "\f505"; } .bi-question-diamond-fill::before { content: "\f506"; } .bi-question-diamond::before { content: "\f507"; } .bi-question-octagon-fill::before { content: "\f508"; } .bi-question-octagon::before { content: "\f509"; } .bi-question-square-fill::before { content: "\f50a"; } .bi-question-square::before { content: "\f50b"; } .bi-question::before { content: "\f50c"; } .bi-rainbow::before { content: "\f50d"; } .bi-receipt-cutoff::before { content: "\f50e"; } .bi-receipt::before { content: "\f50f"; } .bi-reception-0::before { content: "\f510"; } .bi-reception-1::before { content: "\f511"; } .bi-reception-2::before { content: "\f512"; } .bi-reception-3::before { content: "\f513"; } .bi-reception-4::before { content: "\f514"; } .bi-record-btn-fill::before { content: "\f515"; } .bi-record-btn::before { content: "\f516"; } .bi-record-circle-fill::before { content: "\f517"; } .bi-record-circle::before { content: "\f518"; } .bi-record-fill::before { content: "\f519"; } .bi-record::before { content: "\f51a"; } .bi-record2-fill::before { content: "\f51b"; } .bi-record2::before { content: "\f51c"; } .bi-reply-all-fill::before { content: "\f51d"; } .bi-reply-all::before { content: "\f51e"; } .bi-reply-fill::before { content: "\f51f"; } .bi-reply::before { content: "\f520"; } .bi-rss-fill::before { content: "\f521"; } .bi-rss::before { content: "\f522"; } .bi-rulers::before { content: "\f523"; } .bi-save-fill::before { content: "\f524"; } .bi-save::before { content: "\f525"; } .bi-save2-fill::before { content: "\f526"; } .bi-save2::before { content: "\f527"; } .bi-scissors::before { content: "\f528"; } .bi-screwdriver::before { content: "\f529"; } .bi-search::before { content: "\f52a"; } .bi-segmented-nav::before { content: "\f52b"; } .bi-server::before { content: "\f52c"; } .bi-share-fill::before { content: "\f52d"; } .bi-share::before { content: "\f52e"; } .bi-shield-check::before { content: "\f52f"; } .bi-shield-exclamation::before { content: "\f530"; } .bi-shield-fill-check::before { content: "\f531"; } .bi-shield-fill-exclamation::before { content: "\f532"; } .bi-shield-fill-minus::before { content: "\f533"; } .bi-shield-fill-plus::before { content: "\f534"; } .bi-shield-fill-x::before { content: "\f535"; } .bi-shield-fill::before { content: "\f536"; } .bi-shield-lock-fill::before { content: "\f537"; } .bi-shield-lock::before { content: "\f538"; } .bi-shield-minus::before { content: "\f539"; } .bi-shield-plus::before { content: "\f53a"; } .bi-shield-shaded::before { content: "\f53b"; } .bi-shield-slash-fill::before { content: "\f53c"; } .bi-shield-slash::before { content: "\f53d"; } .bi-shield-x::before { content: "\f53e"; } .bi-shield::before { content: "\f53f"; } .bi-shift-fill::before { content: "\f540"; } .bi-shift::before { content: "\f541"; } .bi-shop-window::before { content: "\f542"; } .bi-shop::before { content: "\f543"; } .bi-shuffle::before { content: "\f544"; } .bi-signpost-2-fill::before { content: "\f545"; } .bi-signpost-2::before { content: "\f546"; } .bi-signpost-fill::before { content: "\f547"; } .bi-signpost-split-fill::before { content: "\f548"; } .bi-signpost-split::before { content: "\f549"; } .bi-signpost::before { content: "\f54a"; } .bi-sim-fill::before { content: "\f54b"; } .bi-sim::before { content: "\f54c"; } .bi-skip-backward-btn-fill::before { content: "\f54d"; } .bi-skip-backward-btn::before { content: "\f54e"; } .bi-skip-backward-circle-fill::before { content: "\f54f"; } .bi-skip-backward-circle::before { content: "\f550"; } .bi-skip-backward-fill::before { content: "\f551"; } .bi-skip-backward::before { content: "\f552"; } .bi-skip-end-btn-fill::before { content: "\f553"; } .bi-skip-end-btn::before { content: "\f554"; } .bi-skip-end-circle-fill::before { content: "\f555"; } .bi-skip-end-circle::before { content: "\f556"; } .bi-skip-end-fill::before { content: "\f557"; } .bi-skip-end::before { content: "\f558"; } .bi-skip-forward-btn-fill::before { content: "\f559"; } .bi-skip-forward-btn::before { content: "\f55a"; } .bi-skip-forward-circle-fill::before { content: "\f55b"; } .bi-skip-forward-circle::before { content: "\f55c"; } .bi-skip-forward-fill::before { content: "\f55d"; } .bi-skip-forward::before { content: "\f55e"; } .bi-skip-start-btn-fill::before { content: "\f55f"; } .bi-skip-start-btn::before { content: "\f560"; } .bi-skip-start-circle-fill::before { content: "\f561"; } .bi-skip-start-circle::before { content: "\f562"; } .bi-skip-start-fill::before { content: "\f563"; } .bi-skip-start::before { content: "\f564"; } .bi-slack::before { content: "\f565"; } .bi-slash-circle-fill::before { content: "\f566"; } .bi-slash-circle::before { content: "\f567"; } .bi-slash-square-fill::before { content: "\f568"; } .bi-slash-square::before { content: "\f569"; } .bi-slash::before { content: "\f56a"; } .bi-sliders::before { content: "\f56b"; } .bi-smartwatch::before { content: "\f56c"; } .bi-snow::before { content: "\f56d"; } .bi-snow2::before { content: "\f56e"; } .bi-snow3::before { content: "\f56f"; } .bi-sort-alpha-down-alt::before { content: "\f570"; } .bi-sort-alpha-down::before { content: "\f571"; } .bi-sort-alpha-up-alt::before { content: "\f572"; } .bi-sort-alpha-up::before { content: "\f573"; } .bi-sort-down-alt::before { content: "\f574"; } .bi-sort-down::before { content: "\f575"; } .bi-sort-numeric-down-alt::before { content: "\f576"; } .bi-sort-numeric-down::before { content: "\f577"; } .bi-sort-numeric-up-alt::before { content: "\f578"; } .bi-sort-numeric-up::before { content: "\f579"; } .bi-sort-up-alt::before { content: "\f57a"; } .bi-sort-up::before { content: "\f57b"; } .bi-soundwave::before { content: "\f57c"; } .bi-speaker-fill::before { content: "\f57d"; } .bi-speaker::before { content: "\f57e"; } .bi-speedometer::before { content: "\f57f"; } .bi-speedometer2::before { content: "\f580"; } .bi-spellcheck::before { content: "\f581"; } .bi-square-fill::before { content: "\f582"; } .bi-square-half::before { content: "\f583"; } .bi-square::before { content: "\f584"; } .bi-stack::before { content: "\f585"; } .bi-star-fill::before { content: "\f586"; } .bi-star-half::before { content: "\f587"; } .bi-star::before { content: "\f588"; } .bi-stars::before { content: "\f589"; } .bi-stickies-fill::before { content: "\f58a"; } .bi-stickies::before { content: "\f58b"; } .bi-sticky-fill::before { content: "\f58c"; } .bi-sticky::before { content: "\f58d"; } .bi-stop-btn-fill::before { content: "\f58e"; } .bi-stop-btn::before { content: "\f58f"; } .bi-stop-circle-fill::before { content: "\f590"; } .bi-stop-circle::before { content: "\f591"; } .bi-stop-fill::before { content: "\f592"; } .bi-stop::before { content: "\f593"; } .bi-stoplights-fill::before { content: "\f594"; } .bi-stoplights::before { content: "\f595"; } .bi-stopwatch-fill::before { content: "\f596"; } .bi-stopwatch::before { content: "\f597"; } .bi-subtract::before { content: "\f598"; } .bi-suit-club-fill::before { content: "\f599"; } .bi-suit-club::before { content: "\f59a"; } .bi-suit-diamond-fill::before { content: "\f59b"; } .bi-suit-diamond::before { content: "\f59c"; } .bi-suit-heart-fill::before { content: "\f59d"; } .bi-suit-heart::before { content: "\f59e"; } .bi-suit-spade-fill::before { content: "\f59f"; } .bi-suit-spade::before { content: "\f5a0"; } .bi-sun-fill::before { content: "\f5a1"; } .bi-sun::before { content: "\f5a2"; } .bi-sunglasses::before { content: "\f5a3"; } .bi-sunrise-fill::before { content: "\f5a4"; } .bi-sunrise::before { content: "\f5a5"; } .bi-sunset-fill::before { content: "\f5a6"; } .bi-sunset::before { content: "\f5a7"; } .bi-symmetry-horizontal::before { content: "\f5a8"; } .bi-symmetry-vertical::before { content: "\f5a9"; } .bi-table::before { content: "\f5aa"; } .bi-tablet-fill::before { content: "\f5ab"; } .bi-tablet-landscape-fill::before { content: "\f5ac"; } .bi-tablet-landscape::before { content: "\f5ad"; } .bi-tablet::before { content: "\f5ae"; } .bi-tag-fill::before { content: "\f5af"; } .bi-tag::before { content: "\f5b0"; } .bi-tags-fill::before { content: "\f5b1"; } .bi-tags::before { content: "\f5b2"; } .bi-telegram::before { content: "\f5b3"; } .bi-telephone-fill::before { content: "\f5b4"; } .bi-telephone-forward-fill::before { content: "\f5b5"; } .bi-telephone-forward::before { content: "\f5b6"; } .bi-telephone-inbound-fill::before { content: "\f5b7"; } .bi-telephone-inbound::before { content: "\f5b8"; } .bi-telephone-minus-fill::before { content: "\f5b9"; } .bi-telephone-minus::before { content: "\f5ba"; } .bi-telephone-outbound-fill::before { content: "\f5bb"; } .bi-telephone-outbound::before { content: "\f5bc"; } .bi-telephone-plus-fill::before { content: "\f5bd"; } .bi-telephone-plus::before { content: "\f5be"; } .bi-telephone-x-fill::before { content: "\f5bf"; } .bi-telephone-x::before { content: "\f5c0"; } .bi-telephone::before { content: "\f5c1"; } .bi-terminal-fill::before { content: "\f5c2"; } .bi-terminal::before { content: "\f5c3"; } .bi-text-center::before { content: "\f5c4"; } .bi-text-indent-left::before { content: "\f5c5"; } .bi-text-indent-right::before { content: "\f5c6"; } .bi-text-left::before { content: "\f5c7"; } .bi-text-paragraph::before { content: "\f5c8"; } .bi-text-right::before { content: "\f5c9"; } .bi-textarea-resize::before { content: "\f5ca"; } .bi-textarea-t::before { content: "\f5cb"; } .bi-textarea::before { content: "\f5cc"; } .bi-thermometer-half::before { content: "\f5cd"; } .bi-thermometer-high::before { content: "\f5ce"; } .bi-thermometer-low::before { content: "\f5cf"; } .bi-thermometer-snow::before { content: "\f5d0"; } .bi-thermometer-sun::before { content: "\f5d1"; } .bi-thermometer::before { content: "\f5d2"; } .bi-three-dots-vertical::before { content: "\f5d3"; } .bi-three-dots::before { content: "\f5d4"; } .bi-toggle-off::before { content: "\f5d5"; } .bi-toggle-on::before { content: "\f5d6"; } .bi-toggle2-off::before { content: "\f5d7"; } .bi-toggle2-on::before { content: "\f5d8"; } .bi-toggles::before { content: "\f5d9"; } .bi-toggles2::before { content: "\f5da"; } .bi-tools::before { content: "\f5db"; } .bi-tornado::before { content: "\f5dc"; } .bi-trash-fill::before { content: "\f5dd"; } .bi-trash::before { content: "\f5de"; } .bi-trash2-fill::before { content: "\f5df"; } .bi-trash2::before { content: "\f5e0"; } .bi-tree-fill::before { content: "\f5e1"; } .bi-tree::before { content: "\f5e2"; } .bi-triangle-fill::before { content: "\f5e3"; } .bi-triangle-half::before { content: "\f5e4"; } .bi-triangle::before { content: "\f5e5"; } .bi-trophy-fill::before { content: "\f5e6"; } .bi-trophy::before { content: "\f5e7"; } .bi-tropical-storm::before { content: "\f5e8"; } .bi-truck-flatbed::before { content: "\f5e9"; } .bi-truck::before { content: "\f5ea"; } .bi-tsunami::before { content: "\f5eb"; } .bi-tv-fill::before { content: "\f5ec"; } .bi-tv::before { content: "\f5ed"; } .bi-twitch::before { content: "\f5ee"; } .bi-twitter::before { content: "\f5ef"; } .bi-type-bold::before { content: "\f5f0"; } .bi-type-h1::before { content: "\f5f1"; } .bi-type-h2::before { content: "\f5f2"; } .bi-type-h3::before { content: "\f5f3"; } .bi-type-italic::before { content: "\f5f4"; } .bi-type-strikethrough::before { content: "\f5f5"; } .bi-type-underline::before { content: "\f5f6"; } .bi-type::before { content: "\f5f7"; } .bi-ui-checks-grid::before { content: "\f5f8"; } .bi-ui-checks::before { content: "\f5f9"; } .bi-ui-radios-grid::before { content: "\f5fa"; } .bi-ui-radios::before { content: "\f5fb"; } .bi-umbrella-fill::before { content: "\f5fc"; } .bi-umbrella::before { content: "\f5fd"; } .bi-union::before { content: "\f5fe"; } .bi-unlock-fill::before { content: "\f5ff"; } .bi-unlock::before { content: "\f600"; } .bi-upc-scan::before { content: "\f601"; } .bi-upc::before { content: "\f602"; } .bi-upload::before { content: "\f603"; } .bi-vector-pen::before { content: "\f604"; } .bi-view-list::before { content: "\f605"; } .bi-view-stacked::before { content: "\f606"; } .bi-vinyl-fill::before { content: "\f607"; } .bi-vinyl::before { content: "\f608"; } .bi-voicemail::before { content: "\f609"; } .bi-volume-down-fill::before { content: "\f60a"; } .bi-volume-down::before { content: "\f60b"; } .bi-volume-mute-fill::before { content: "\f60c"; } .bi-volume-mute::before { content: "\f60d"; } .bi-volume-off-fill::before { content: "\f60e"; } .bi-volume-off::before { content: "\f60f"; } .bi-volume-up-fill::before { content: "\f610"; } .bi-volume-up::before { content: "\f611"; } .bi-vr::before { content: "\f612"; } .bi-wallet-fill::before { content: "\f613"; } .bi-wallet::before { content: "\f614"; } .bi-wallet2::before { content: "\f615"; } .bi-watch::before { content: "\f616"; } .bi-water::before { content: "\f617"; } .bi-whatsapp::before { content: "\f618"; } .bi-wifi-1::before { content: "\f619"; } .bi-wifi-2::before { content: "\f61a"; } .bi-wifi-off::before { content: "\f61b"; } .bi-wifi::before { content: "\f61c"; } .bi-wind::before { content: "\f61d"; } .bi-window-dock::before { content: "\f61e"; } .bi-window-sidebar::before { content: "\f61f"; } .bi-window::before { content: "\f620"; } .bi-wrench::before { content: "\f621"; } .bi-x-circle-fill::before { content: "\f622"; } .bi-x-circle::before { content: "\f623"; } .bi-x-diamond-fill::before { content: "\f624"; } .bi-x-diamond::before { content: "\f625"; } .bi-x-octagon-fill::before { content: "\f626"; } .bi-x-octagon::before { content: "\f627"; } .bi-x-square-fill::before { content: "\f628"; } .bi-x-square::before { content: "\f629"; } .bi-x::before { content: "\f62a"; } .bi-youtube::before { content: "\f62b"; } .bi-zoom-in::before { content: "\f62c"; } .bi-zoom-out::before { content: "\f62d"; } .bi-bank::before { content: "\f62e"; } .bi-bank2::before { content: "\f62f"; } .bi-bell-slash-fill::before { content: "\f630"; } .bi-bell-slash::before { content: "\f631"; } .bi-cash-coin::before { content: "\f632"; } .bi-check-lg::before { content: "\f633"; } .bi-coin::before { content: "\f634"; } .bi-currency-bitcoin::before { content: "\f635"; } .bi-currency-dollar::before { content: "\f636"; } .bi-currency-euro::before { content: "\f637"; } .bi-currency-exchange::before { content: "\f638"; } .bi-currency-pound::before { content: "\f639"; } .bi-currency-yen::before { content: "\f63a"; } .bi-dash-lg::before { content: "\f63b"; } .bi-exclamation-lg::before { content: "\f63c"; } .bi-file-earmark-pdf-fill::before { content: "\f63d"; } .bi-file-earmark-pdf::before { content: "\f63e"; } .bi-file-pdf-fill::before { content: "\f63f"; } .bi-file-pdf::before { content: "\f640"; } .bi-gender-ambiguous::before { content: "\f641"; } .bi-gender-female::before { content: "\f642"; } .bi-gender-male::before { content: "\f643"; } .bi-gender-trans::before { content: "\f644"; } .bi-headset-vr::before { content: "\f645"; } .bi-info-lg::before { content: "\f646"; } .bi-mastodon::before { content: "\f647"; } .bi-messenger::before { content: "\f648"; } .bi-piggy-bank-fill::before { content: "\f649"; } .bi-piggy-bank::before { content: "\f64a"; } .bi-pin-map-fill::before { content: "\f64b"; } .bi-pin-map::before { content: "\f64c"; } .bi-plus-lg::before { content: "\f64d"; } .bi-question-lg::before { content: "\f64e"; } .bi-recycle::before { content: "\f64f"; } .bi-reddit::before { content: "\f650"; } .bi-safe-fill::before { content: "\f651"; } .bi-safe2-fill::before { content: "\f652"; } .bi-safe2::before { content: "\f653"; } .bi-sd-card-fill::before { content: "\f654"; } .bi-sd-card::before { content: "\f655"; } .bi-skype::before { content: "\f656"; } .bi-slash-lg::before { content: "\f657"; } .bi-translate::before { content: "\f658"; } .bi-x-lg::before { content: "\f659"; } .bi-safe::before { content: "\f65a"; } .bi-apple::before { content: "\f65b"; } .bi-microsoft::before { content: "\f65d"; } .bi-windows::before { content: "\f65e"; } .bi-behance::before { content: "\f65c"; } .bi-dribbble::before { content: "\f65f"; } .bi-line::before { content: "\f660"; } .bi-medium::before { content: "\f661"; } .bi-paypal::before { content: "\f662"; } .bi-pinterest::before { content: "\f663"; } .bi-signal::before { content: "\f664"; } .bi-snapchat::before { content: "\f665"; } .bi-spotify::before { content: "\f666"; } .bi-stack-overflow::before { content: "\f667"; } .bi-strava::before { content: "\f668"; } .bi-wordpress::before { content: "\f669"; } .bi-vimeo::before { content: "\f66a"; } .bi-activity::before { content: "\f66b"; } .bi-easel2-fill::before { content: "\f66c"; } .bi-easel2::before { content: "\f66d"; } .bi-easel3-fill::before { content: "\f66e"; } .bi-easel3::before { content: "\f66f"; } .bi-fan::before { content: "\f670"; } .bi-fingerprint::before { content: "\f671"; } .bi-graph-down-arrow::before { content: "\f672"; } .bi-graph-up-arrow::before { content: "\f673"; } .bi-hypnotize::before { content: "\f674"; } .bi-magic::before { content: "\f675"; } .bi-person-rolodex::before { content: "\f676"; } .bi-person-video::before { content: "\f677"; } .bi-person-video2::before { content: "\f678"; } .bi-person-video3::before { content: "\f679"; } .bi-person-workspace::before { content: "\f67a"; } .bi-radioactive::before { content: "\f67b"; } .bi-webcam-fill::before { content: "\f67c"; } .bi-webcam::before { content: "\f67d"; } .bi-yin-yang::before { content: "\f67e"; } .bi-bandaid-fill::before { content: "\f680"; } .bi-bandaid::before { content: "\f681"; } .bi-bluetooth::before { content: "\f682"; } .bi-body-text::before { content: "\f683"; } .bi-boombox::before { content: "\f684"; } .bi-boxes::before { content: "\f685"; } .bi-dpad-fill::before { content: "\f686"; } .bi-dpad::before { content: "\f687"; } .bi-ear-fill::before { content: "\f688"; } .bi-ear::before { content: "\f689"; } .bi-envelope-check-1::before { content: "\f68a"; } .bi-envelope-check-fill::before { content: "\f68b"; } .bi-envelope-check::before { content: "\f68c"; } .bi-envelope-dash-1::before { content: "\f68d"; } .bi-envelope-dash-fill::before { content: "\f68e"; } .bi-envelope-dash::before { content: "\f68f"; } .bi-envelope-exclamation-1::before { content: "\f690"; } .bi-envelope-exclamation-fill::before { content: "\f691"; } .bi-envelope-exclamation::before { content: "\f692"; } .bi-envelope-plus-fill::before { content: "\f693"; } .bi-envelope-plus::before { content: "\f694"; } .bi-envelope-slash-1::before { content: "\f695"; } .bi-envelope-slash-fill::before { content: "\f696"; } .bi-envelope-slash::before { content: "\f697"; } .bi-envelope-x-1::before { content: "\f698"; } .bi-envelope-x-fill::before { content: "\f699"; } .bi-envelope-x::before { content: "\f69a"; } .bi-explicit-fill::before { content: "\f69b"; } .bi-explicit::before { content: "\f69c"; } .bi-git::before { content: "\f69d"; } .bi-infinity::before { content: "\f69e"; } .bi-list-columns-reverse::before { content: "\f69f"; } .bi-list-columns::before { content: "\f6a0"; } .bi-meta::before { content: "\f6a1"; } .bi-mortorboard-fill::before { content: "\f6a2"; } .bi-mortorboard::before { content: "\f6a3"; } .bi-nintendo-switch::before { content: "\f6a4"; } .bi-pc-display-horizontal::before { content: "\f6a5"; } .bi-pc-display::before { content: "\f6a6"; } .bi-pc-horizontal::before { content: "\f6a7"; } .bi-pc::before { content: "\f6a8"; } .bi-playstation::before { content: "\f6a9"; } .bi-plus-slash-minus::before { content: "\f6aa"; } .bi-projector-fill::before { content: "\f6ab"; } .bi-projector::before { content: "\f6ac"; } .bi-qr-code-scan::before { content: "\f6ad"; } .bi-qr-code::before { content: "\f6ae"; } .bi-quora::before { content: "\f6af"; } .bi-quote::before { content: "\f6b0"; } .bi-robot::before { content: "\f6b1"; } .bi-send-check-fill::before { content: "\f6b2"; } .bi-send-check::before { content: "\f6b3"; } .bi-send-dash-fill::before { content: "\f6b4"; } .bi-send-dash::before { content: "\f6b5"; } .bi-send-exclamation-1::before { content: "\f6b6"; } .bi-send-exclamation-fill::before { content: "\f6b7"; } .bi-send-exclamation::before { content: "\f6b8"; } .bi-send-fill::before { content: "\f6b9"; } .bi-send-plus-fill::before { content: "\f6ba"; } .bi-send-plus::before { content: "\f6bb"; } .bi-send-slash-fill::before { content: "\f6bc"; } .bi-send-slash::before { content: "\f6bd"; } .bi-send-x-fill::before { content: "\f6be"; } .bi-send-x::before { content: "\f6bf"; } .bi-send::before { content: "\f6c0"; } .bi-steam::before { content: "\f6c1"; } .bi-terminal-dash-1::before { content: "\f6c2"; } .bi-terminal-dash::before { content: "\f6c3"; } .bi-terminal-plus::before { content: "\f6c4"; } .bi-terminal-split::before { content: "\f6c5"; } .bi-ticket-detailed-fill::before { content: "\f6c6"; } .bi-ticket-detailed::before { content: "\f6c7"; } .bi-ticket-fill::before { content: "\f6c8"; } .bi-ticket-perforated-fill::before { content: "\f6c9"; } .bi-ticket-perforated::before { content: "\f6ca"; } .bi-ticket::before { content: "\f6cb"; } .bi-tiktok::before { content: "\f6cc"; } .bi-window-dash::before { content: "\f6cd"; } .bi-window-desktop::before { content: "\f6ce"; } .bi-window-fullscreen::before { content: "\f6cf"; } .bi-window-plus::before { content: "\f6d0"; } .bi-window-split::before { content: "\f6d1"; } .bi-window-stack::before { content: "\f6d2"; } .bi-window-x::before { content: "\f6d3"; } .bi-xbox::before { content: "\f6d4"; } .bi-ethernet::before { content: "\f6d5"; } .bi-hdmi-fill::before { content: "\f6d6"; } .bi-hdmi::before { content: "\f6d7"; } .bi-usb-c-fill::before { content: "\f6d8"; } .bi-usb-c::before { content: "\f6d9"; } .bi-usb-fill::before { content: "\f6da"; } .bi-usb-plug-fill::before { content: "\f6db"; } .bi-usb-plug::before { content: "\f6dc"; } .bi-usb-symbol::before { content: "\f6dd"; } .bi-usb::before { content: "\f6de"; } .bi-boombox-fill::before { content: "\f6df"; } .bi-displayport-1::before { content: "\f6e0"; } .bi-displayport::before { content: "\f6e1"; } .bi-gpu-card::before { content: "\f6e2"; } .bi-memory::before { content: "\f6e3"; } .bi-modem-fill::before { content: "\f6e4"; } .bi-modem::before { content: "\f6e5"; } .bi-motherboard-fill::before { content: "\f6e6"; } .bi-motherboard::before { content: "\f6e7"; } .bi-optical-audio-fill::before { content: "\f6e8"; } .bi-optical-audio::before { content: "\f6e9"; } .bi-pci-card::before { content: "\f6ea"; } .bi-router-fill::before { content: "\f6eb"; } .bi-router::before { content: "\f6ec"; } .bi-ssd-fill::before { content: "\f6ed"; } .bi-ssd::before { content: "\f6ee"; } .bi-thunderbolt-fill::before { content: "\f6ef"; } .bi-thunderbolt::before { content: "\f6f0"; } .bi-usb-drive-fill::before { content: "\f6f1"; } .bi-usb-drive::before { content: "\f6f2"; } .bi-usb-micro-fill::before { content: "\f6f3"; } .bi-usb-micro::before { content: "\f6f4"; } .bi-usb-mini-fill::before { content: "\f6f5"; } .bi-usb-mini::before { content: "\f6f6"; } .bi-cloud-haze2::before { content: "\f6f7"; } .bi-device-hdd-fill::before { content: "\f6f8"; } .bi-device-hdd::before { content: "\f6f9"; } .bi-device-ssd-fill::before { content: "\f6fa"; } .bi-device-ssd::before { content: "\f6fb"; } .bi-displayport-fill::before { content: "\f6fc"; } .bi-mortarboard-fill::before { content: "\f6fd"; } .bi-mortarboard::before { content: "\f6fe"; } .bi-terminal-x::before { content: "\f6ff"; } .bi-arrow-through-heart-fill::before { content: "\f700"; } .bi-arrow-through-heart::before { content: "\f701"; } .bi-badge-sd-fill::before { content: "\f702"; } .bi-badge-sd::before { content: "\f703"; } .bi-bag-heart-fill::before { content: "\f704"; } .bi-bag-heart::before { content: "\f705"; } .bi-balloon-fill::before { content: "\f706"; } .bi-balloon-heart-fill::before { content: "\f707"; } .bi-balloon-heart::before { content: "\f708"; } .bi-balloon::before { content: "\f709"; } .bi-box2-fill::before { content: "\f70a"; } .bi-box2-heart-fill::before { content: "\f70b"; } .bi-box2-heart::before { content: "\f70c"; } .bi-box2::before { content: "\f70d"; } .bi-braces-asterisk::before { content: "\f70e"; } .bi-calendar-heart-fill::before { content: "\f70f"; } .bi-calendar-heart::before { content: "\f710"; } .bi-calendar2-heart-fill::before { content: "\f711"; } .bi-calendar2-heart::before { content: "\f712"; } .bi-chat-heart-fill::before { content: "\f713"; } .bi-chat-heart::before { content: "\f714"; } .bi-chat-left-heart-fill::before { content: "\f715"; } .bi-chat-left-heart::before { content: "\f716"; } .bi-chat-right-heart-fill::before { content: "\f717"; } .bi-chat-right-heart::before { content: "\f718"; } .bi-chat-square-heart-fill::before { content: "\f719"; } .bi-chat-square-heart::before { content: "\f71a"; } .bi-clipboard-check-fill::before { content: "\f71b"; } .bi-clipboard-data-fill::before { content: "\f71c"; } .bi-clipboard-fill::before { content: "\f71d"; } .bi-clipboard-heart-fill::before { content: "\f71e"; } .bi-clipboard-heart::before { content: "\f71f"; } .bi-clipboard-minus-fill::before { content: "\f720"; } .bi-clipboard-plus-fill::before { content: "\f721"; } .bi-clipboard-pulse::before { content: "\f722"; } .bi-clipboard-x-fill::before { content: "\f723"; } .bi-clipboard2-check-fill::before { content: "\f724"; } .bi-clipboard2-check::before { content: "\f725"; } .bi-clipboard2-data-fill::before { content: "\f726"; } .bi-clipboard2-data::before { content: "\f727"; } .bi-clipboard2-fill::before { content: "\f728"; } .bi-clipboard2-heart-fill::before { content: "\f729"; } .bi-clipboard2-heart::before { content: "\f72a"; } .bi-clipboard2-minus-fill::before { content: "\f72b"; } .bi-clipboard2-minus::before { content: "\f72c"; } .bi-clipboard2-plus-fill::before { content: "\f72d"; } .bi-clipboard2-plus::before { content: "\f72e"; } .bi-clipboard2-pulse-fill::before { content: "\f72f"; } .bi-clipboard2-pulse::before { content: "\f730"; } .bi-clipboard2-x-fill::before { content: "\f731"; } .bi-clipboard2-x::before { content: "\f732"; } .bi-clipboard2::before { content: "\f733"; } .bi-emoji-kiss-fill::before { content: "\f734"; } .bi-emoji-kiss::before { content: "\f735"; } .bi-envelope-heart-fill::before { content: "\f736"; } .bi-envelope-heart::before { content: "\f737"; } .bi-envelope-open-heart-fill::before { content: "\f738"; } .bi-envelope-open-heart::before { content: "\f739"; } .bi-envelope-paper-fill::before { content: "\f73a"; } .bi-envelope-paper-heart-fill::before { content: "\f73b"; } .bi-envelope-paper-heart::before { content: "\f73c"; } .bi-envelope-paper::before { content: "\f73d"; } .bi-filetype-aac::before { content: "\f73e"; } .bi-filetype-ai::before { content: "\f73f"; } .bi-filetype-bmp::before { content: "\f740"; } .bi-filetype-cs::before { content: "\f741"; } .bi-filetype-css::before { content: "\f742"; } .bi-filetype-csv::before { content: "\f743"; } .bi-filetype-doc::before { content: "\f744"; } .bi-filetype-docx::before { content: "\f745"; } .bi-filetype-exe::before { content: "\f746"; } .bi-filetype-gif::before { content: "\f747"; } .bi-filetype-heic::before { content: "\f748"; } .bi-filetype-html::before { content: "\f749"; } .bi-filetype-java::before { content: "\f74a"; } .bi-filetype-jpg::before { content: "\f74b"; } .bi-filetype-js::before { content: "\f74c"; } .bi-filetype-jsx::before { content: "\f74d"; } .bi-filetype-key::before { content: "\f74e"; } .bi-filetype-m4p::before { content: "\f74f"; } .bi-filetype-md::before { content: "\f750"; } .bi-filetype-mdx::before { content: "\f751"; } .bi-filetype-mov::before { content: "\f752"; } .bi-filetype-mp3::before { content: "\f753"; } .bi-filetype-mp4::before { content: "\f754"; } .bi-filetype-otf::before { content: "\f755"; } .bi-filetype-pdf::before { content: "\f756"; } .bi-filetype-php::before { content: "\f757"; } .bi-filetype-png::before { content: "\f758"; } .bi-filetype-ppt-1::before { content: "\f759"; } .bi-filetype-ppt::before { content: "\f75a"; } .bi-filetype-psd::before { content: "\f75b"; } .bi-filetype-py::before { content: "\f75c"; } .bi-filetype-raw::before { content: "\f75d"; } .bi-filetype-rb::before { content: "\f75e"; } .bi-filetype-sass::before { content: "\f75f"; } .bi-filetype-scss::before { content: "\f760"; } .bi-filetype-sh::before { content: "\f761"; } .bi-filetype-svg::before { content: "\f762"; } .bi-filetype-tiff::before { content: "\f763"; } .bi-filetype-tsx::before { content: "\f764"; } .bi-filetype-ttf::before { content: "\f765"; } .bi-filetype-txt::before { content: "\f766"; } .bi-filetype-wav::before { content: "\f767"; } .bi-filetype-woff::before { content: "\f768"; } .bi-filetype-xls-1::before { content: "\f769"; } .bi-filetype-xls::before { content: "\f76a"; } .bi-filetype-xml::before { content: "\f76b"; } .bi-filetype-yml::before { content: "\f76c"; } .bi-heart-arrow::before { content: "\f76d"; } .bi-heart-pulse-fill::before { content: "\f76e"; } .bi-heart-pulse::before { content: "\f76f"; } .bi-heartbreak-fill::before { content: "\f770"; } .bi-heartbreak::before { content: "\f771"; } .bi-hearts::before { content: "\f772"; } .bi-hospital-fill::before { content: "\f773"; } .bi-hospital::before { content: "\f774"; } .bi-house-heart-fill::before { content: "\f775"; } .bi-house-heart::before { content: "\f776"; } .bi-incognito::before { content: "\f777"; } .bi-magnet-fill::before { content: "\f778"; } .bi-magnet::before { content: "\f779"; } .bi-person-heart::before { content: "\f77a"; } .bi-person-hearts::before { content: "\f77b"; } .bi-phone-flip::before { content: "\f77c"; } .bi-plugin::before { content: "\f77d"; } .bi-postage-fill::before { content: "\f77e"; } .bi-postage-heart-fill::before { content: "\f77f"; } .bi-postage-heart::before { content: "\f780"; } .bi-postage::before { content: "\f781"; } .bi-postcard-fill::before { content: "\f782"; } .bi-postcard-heart-fill::before { content: "\f783"; } .bi-postcard-heart::before { content: "\f784"; } .bi-postcard::before { content: "\f785"; } .bi-search-heart-fill::before { content: "\f786"; } .bi-search-heart::before { content: "\f787"; } .bi-sliders2-vertical::before { content: "\f788"; } .bi-sliders2::before { content: "\f789"; } .bi-trash3-fill::before { content: "\f78a"; } .bi-trash3::before { content: "\f78b"; } .bi-valentine::before { content: "\f78c"; } .bi-valentine2::before { content: "\f78d"; } .bi-wrench-adjustable-circle-fill::before { content: "\f78e"; } .bi-wrench-adjustable-circle::before { content: "\f78f"; } .bi-wrench-adjustable::before { content: "\f790"; } .bi-filetype-json::before { content: "\f791"; } .bi-filetype-pptx::before { content: "\f792"; } .bi-filetype-xlsx::before { content: "\f793"; } .bi-1-circle-1::before { content: "\f794"; } .bi-1-circle-fill-1::before { content: "\f795"; } .bi-1-circle-fill::before { content: "\f796"; } .bi-1-circle::before { content: "\f797"; } .bi-1-square-fill::before { content: "\f798"; } .bi-1-square::before { content: "\f799"; } .bi-2-circle-1::before { content: "\f79a"; } .bi-2-circle-fill-1::before { content: "\f79b"; } .bi-2-circle-fill::before { content: "\f79c"; } .bi-2-circle::before { content: "\f79d"; } .bi-2-square-fill::before { content: "\f79e"; } .bi-2-square::before { content: "\f79f"; } .bi-3-circle-1::before { content: "\f7a0"; } .bi-3-circle-fill-1::before { content: "\f7a1"; } .bi-3-circle-fill::before { content: "\f7a2"; } .bi-3-circle::before { content: "\f7a3"; } .bi-3-square-fill::before { content: "\f7a4"; } .bi-3-square::before { content: "\f7a5"; } .bi-4-circle-1::before { content: "\f7a6"; } .bi-4-circle-fill-1::before { content: "\f7a7"; } .bi-4-circle-fill::before { content: "\f7a8"; } .bi-4-circle::before { content: "\f7a9"; } .bi-4-square-fill::before { content: "\f7aa"; } .bi-4-square::before { content: "\f7ab"; } .bi-5-circle-1::before { content: "\f7ac"; } .bi-5-circle-fill-1::before { content: "\f7ad"; } .bi-5-circle-fill::before { content: "\f7ae"; } .bi-5-circle::before { content: "\f7af"; } .bi-5-square-fill::before { content: "\f7b0"; } .bi-5-square::before { content: "\f7b1"; } .bi-6-circle-1::before { content: "\f7b2"; } .bi-6-circle-fill-1::before { content: "\f7b3"; } .bi-6-circle-fill::before { content: "\f7b4"; } .bi-6-circle::before { content: "\f7b5"; } .bi-6-square-fill::before { content: "\f7b6"; } .bi-6-square::before { content: "\f7b7"; } .bi-7-circle-1::before { content: "\f7b8"; } .bi-7-circle-fill-1::before { content: "\f7b9"; } .bi-7-circle-fill::before { content: "\f7ba"; } .bi-7-circle::before { content: "\f7bb"; } .bi-7-square-fill::before { content: "\f7bc"; } .bi-7-square::before { content: "\f7bd"; } .bi-8-circle-1::before { content: "\f7be"; } .bi-8-circle-fill-1::before { content: "\f7bf"; } .bi-8-circle-fill::before { content: "\f7c0"; } .bi-8-circle::before { content: "\f7c1"; } .bi-8-square-fill::before { content: "\f7c2"; } .bi-8-square::before { content: "\f7c3"; } .bi-9-circle-1::before { content: "\f7c4"; } .bi-9-circle-fill-1::before { content: "\f7c5"; } .bi-9-circle-fill::before { content: "\f7c6"; } .bi-9-circle::before { content: "\f7c7"; } .bi-9-square-fill::before { content: "\f7c8"; } .bi-9-square::before { content: "\f7c9"; } .bi-airplane-engines-fill::before { content: "\f7ca"; } .bi-airplane-engines::before { content: "\f7cb"; } .bi-airplane-fill::before { content: "\f7cc"; } .bi-airplane::before { content: "\f7cd"; } .bi-alexa::before { content: "\f7ce"; } .bi-alipay::before { content: "\f7cf"; } .bi-android::before { content: "\f7d0"; } .bi-android2::before { content: "\f7d1"; } .bi-box-fill::before { content: "\f7d2"; } .bi-box-seam-fill::before { content: "\f7d3"; } .bi-browser-chrome::before { content: "\f7d4"; } .bi-browser-edge::before { content: "\f7d5"; } .bi-browser-firefox::before { content: "\f7d6"; } .bi-browser-safari::before { content: "\f7d7"; } .bi-c-circle-1::before { content: "\f7d8"; } .bi-c-circle-fill-1::before { content: "\f7d9"; } .bi-c-circle-fill::before { content: "\f7da"; } .bi-c-circle::before { content: "\f7db"; } .bi-c-square-fill::before { content: "\f7dc"; } .bi-c-square::before { content: "\f7dd"; } .bi-capsule-pill::before { content: "\f7de"; } .bi-capsule::before { content: "\f7df"; } .bi-car-front-fill::before { content: "\f7e0"; } .bi-car-front::before { content: "\f7e1"; } .bi-cassette-fill::before { content: "\f7e2"; } .bi-cassette::before { content: "\f7e3"; } .bi-cc-circle-1::before { content: "\f7e4"; } .bi-cc-circle-fill-1::before { content: "\f7e5"; } .bi-cc-circle-fill::before { content: "\f7e6"; } .bi-cc-circle::before { content: "\f7e7"; } .bi-cc-square-fill::before { content: "\f7e8"; } .bi-cc-square::before { content: "\f7e9"; } .bi-cup-hot-fill::before { content: "\f7ea"; } .bi-cup-hot::before { content: "\f7eb"; } .bi-currency-rupee::before { content: "\f7ec"; } .bi-dropbox::before { content: "\f7ed"; } .bi-escape::before { content: "\f7ee"; } .bi-fast-forward-btn-fill::before { content: "\f7ef"; } .bi-fast-forward-btn::before { content: "\f7f0"; } .bi-fast-forward-circle-fill::before { content: "\f7f1"; } .bi-fast-forward-circle::before { content: "\f7f2"; } .bi-fast-forward-fill::before { content: "\f7f3"; } .bi-fast-forward::before { content: "\f7f4"; } .bi-filetype-sql::before { content: "\f7f5"; } .bi-fire::before { content: "\f7f6"; } .bi-google-play::before { content: "\f7f7"; } .bi-h-circle-1::before { content: "\f7f8"; } .bi-h-circle-fill-1::before { content: "\f7f9"; } .bi-h-circle-fill::before { content: "\f7fa"; } .bi-h-circle::before { content: "\f7fb"; } .bi-h-square-fill::before { content: "\f7fc"; } .bi-h-square::before { content: "\f7fd"; } .bi-indent::before { content: "\f7fe"; } .bi-lungs-fill::before { content: "\f7ff"; } .bi-lungs::before { content: "\f800"; } .bi-microsoft-teams::before { content: "\f801"; } .bi-p-circle-1::before { content: "\f802"; } .bi-p-circle-fill-1::before { content: "\f803"; } .bi-p-circle-fill::before { content: "\f804"; } .bi-p-circle::before { content: "\f805"; } .bi-p-square-fill::before { content: "\f806"; } .bi-p-square::before { content: "\f807"; } .bi-pass-fill::before { content: "\f808"; } .bi-pass::before { content: "\f809"; } .bi-prescription::before { content: "\f80a"; } .bi-prescription2::before { content: "\f80b"; } .bi-r-circle-1::before { content: "\f80c"; } .bi-r-circle-fill-1::before { content: "\f80d"; } .bi-r-circle-fill::before { content: "\f80e"; } .bi-r-circle::before { content: "\f80f"; } .bi-r-square-fill::before { content: "\f810"; } .bi-r-square::before { content: "\f811"; } .bi-repeat-1::before { content: "\f812"; } .bi-repeat::before { content: "\f813"; } .bi-rewind-btn-fill::before { content: "\f814"; } .bi-rewind-btn::before { content: "\f815"; } .bi-rewind-circle-fill::before { content: "\f816"; } .bi-rewind-circle::before { content: "\f817"; } .bi-rewind-fill::before { content: "\f818"; } .bi-rewind::before { content: "\f819"; } .bi-train-freight-front-fill::before { content: "\f81a"; } .bi-train-freight-front::before { content: "\f81b"; } .bi-train-front-fill::before { content: "\f81c"; } .bi-train-front::before { content: "\f81d"; } .bi-train-lightrail-front-fill::before { content: "\f81e"; } .bi-train-lightrail-front::before { content: "\f81f"; } .bi-truck-front-fill::before { content: "\f820"; } .bi-truck-front::before { content: "\f821"; } .bi-ubuntu::before { content: "\f822"; } .bi-unindent::before { content: "\f823"; } .bi-unity::before { content: "\f824"; } .bi-universal-access-circle::before { content: "\f825"; } .bi-universal-access::before { content: "\f826"; } .bi-virus::before { content: "\f827"; } .bi-virus2::before { content: "\f828"; } .bi-wechat::before { content: "\f829"; } .bi-yelp::before { content: "\f82a"; } .bi-sign-stop-fill::before { content: "\f82b"; } .bi-sign-stop-lights-fill::before { content: "\f82c"; } .bi-sign-stop-lights::before { content: "\f82d"; } .bi-sign-stop::before { content: "\f82e"; } .bi-sign-turn-left-fill::before { content: "\f82f"; } .bi-sign-turn-left::before { content: "\f830"; } .bi-sign-turn-right-fill::before { content: "\f831"; } .bi-sign-turn-right::before { content: "\f832"; } .bi-sign-turn-slight-left-fill::before { content: "\f833"; } .bi-sign-turn-slight-left::before { content: "\f834"; } .bi-sign-turn-slight-right-fill::before { content: "\f835"; } .bi-sign-turn-slight-right::before { content: "\f836"; } .bi-sign-yield-fill::before { content: "\f837"; } .bi-sign-yield::before { content: "\f838"; } .bi-ev-station-fill::before { content: "\f839"; } .bi-ev-station::before { content: "\f83a"; } .bi-fuel-pump-diesel-fill::before { content: "\f83b"; } .bi-fuel-pump-diesel::before { content: "\f83c"; } .bi-fuel-pump-fill::before { content: "\f83d"; } .bi-fuel-pump::before { content: "\f83e"; } ================================================ FILE: src/electionguard_gui/web/css/bootstrap-overrides.css ================================================ .bg-primary { background-color: #009688 !important; } .btn-primary { --bs-btn-bg: #009688 !important; --bs-btn-border-color: teal !important; --bs-btn-hover-bg: #008477 !important; --bs-btn-hover-border-color: #005d53 !important; --bs-btn-active-bg: #005d53 !important; --bs-btn-active-border-color: #005d53 !important; } ================================================ FILE: src/electionguard_gui/web/css/eg-styles.css ================================================ .key-ceremony-status { font-size: 1.2rem; font-weight: bold; } ================================================ FILE: src/electionguard_gui/web/css/spinner.css ================================================ /* via https://cssload.net/en/spinners */ .windows8 { position: relative; width: 30px; height: 30px; margin: auto; } .windows8 .wBall { position: absolute; width: 28px; height: 28px; opacity: 0; transform: rotate(225deg); -o-transform: rotate(225deg); -ms-transform: rotate(225deg); -webkit-transform: rotate(225deg); -moz-transform: rotate(225deg); animation: orbit 6.96s infinite; -o-animation: orbit 6.96s infinite; -ms-animation: orbit 6.96s infinite; -webkit-animation: orbit 6.96s infinite; -moz-animation: orbit 6.96s infinite; } .windows8 .wBall .wInnerBall { position: absolute; width: 4px; height: 4px; background: rgb(0, 150, 136); left: 0px; top: 0px; border-radius: 4px; } .windows8 #wBall_1 { animation-delay: 1.52s; -o-animation-delay: 1.52s; -ms-animation-delay: 1.52s; -webkit-animation-delay: 1.52s; -moz-animation-delay: 1.52s; } .windows8 #wBall_2 { animation-delay: 0.3s; -o-animation-delay: 0.3s; -ms-animation-delay: 0.3s; -webkit-animation-delay: 0.3s; -moz-animation-delay: 0.3s; } .windows8 #wBall_3 { animation-delay: 0.61s; -o-animation-delay: 0.61s; -ms-animation-delay: 0.61s; -webkit-animation-delay: 0.61s; -moz-animation-delay: 0.61s; } .windows8 #wBall_4 { animation-delay: 0.91s; -o-animation-delay: 0.91s; -ms-animation-delay: 0.91s; -webkit-animation-delay: 0.91s; -moz-animation-delay: 0.91s; } .windows8 #wBall_5 { animation-delay: 1.22s; -o-animation-delay: 1.22s; -ms-animation-delay: 1.22s; -webkit-animation-delay: 1.22s; -moz-animation-delay: 1.22s; } @keyframes orbit { 0% { opacity: 1; z-index: 99; transform: rotate(180deg); animation-timing-function: ease-out; } 7% { opacity: 1; transform: rotate(300deg); animation-timing-function: linear; origin: 0%; } 30% { opacity: 1; transform: rotate(410deg); animation-timing-function: ease-in-out; origin: 7%; } 39% { opacity: 1; transform: rotate(645deg); animation-timing-function: linear; origin: 30%; } 70% { opacity: 1; transform: rotate(770deg); animation-timing-function: ease-out; origin: 39%; } 75% { opacity: 1; transform: rotate(900deg); animation-timing-function: ease-out; origin: 70%; } 76% { opacity: 0; transform: rotate(900deg); } 100% { opacity: 0; transform: rotate(900deg); } } @-o-keyframes orbit { 0% { opacity: 1; z-index: 99; -o-transform: rotate(180deg); -o-animation-timing-function: ease-out; } 7% { opacity: 1; -o-transform: rotate(300deg); -o-animation-timing-function: linear; -o-origin: 0%; } 30% { opacity: 1; -o-transform: rotate(410deg); -o-animation-timing-function: ease-in-out; -o-origin: 7%; } 39% { opacity: 1; -o-transform: rotate(645deg); -o-animation-timing-function: linear; -o-origin: 30%; } 70% { opacity: 1; -o-transform: rotate(770deg); -o-animation-timing-function: ease-out; -o-origin: 39%; } 75% { opacity: 1; -o-transform: rotate(900deg); -o-animation-timing-function: ease-out; -o-origin: 70%; } 76% { opacity: 0; -o-transform: rotate(900deg); } 100% { opacity: 0; -o-transform: rotate(900deg); } } @-ms-keyframes orbit { 0% { opacity: 1; z-index: 99; -ms-transform: rotate(180deg); -ms-animation-timing-function: ease-out; } 7% { opacity: 1; -ms-transform: rotate(300deg); -ms-animation-timing-function: linear; -ms-origin: 0%; } 30% { opacity: 1; -ms-transform: rotate(410deg); -ms-animation-timing-function: ease-in-out; -ms-origin: 7%; } 39% { opacity: 1; -ms-transform: rotate(645deg); -ms-animation-timing-function: linear; -ms-origin: 30%; } 70% { opacity: 1; -ms-transform: rotate(770deg); -ms-animation-timing-function: ease-out; -ms-origin: 39%; } 75% { opacity: 1; -ms-transform: rotate(900deg); -ms-animation-timing-function: ease-out; -ms-origin: 70%; } 76% { opacity: 0; -ms-transform: rotate(900deg); } 100% { opacity: 0; -ms-transform: rotate(900deg); } } @-webkit-keyframes orbit { 0% { opacity: 1; z-index: 99; -webkit-transform: rotate(180deg); -webkit-animation-timing-function: ease-out; } 7% { opacity: 1; -webkit-transform: rotate(300deg); -webkit-animation-timing-function: linear; -webkit-origin: 0%; } 30% { opacity: 1; -webkit-transform: rotate(410deg); -webkit-animation-timing-function: ease-in-out; -webkit-origin: 7%; } 39% { opacity: 1; -webkit-transform: rotate(645deg); -webkit-animation-timing-function: linear; -webkit-origin: 30%; } 70% { opacity: 1; -webkit-transform: rotate(770deg); -webkit-animation-timing-function: ease-out; -webkit-origin: 39%; } 75% { opacity: 1; -webkit-transform: rotate(900deg); -webkit-animation-timing-function: ease-out; -webkit-origin: 70%; } 76% { opacity: 0; -webkit-transform: rotate(900deg); } 100% { opacity: 0; -webkit-transform: rotate(900deg); } } @-moz-keyframes orbit { 0% { opacity: 1; z-index: 99; -moz-transform: rotate(180deg); -moz-animation-timing-function: ease-out; } 7% { opacity: 1; -moz-transform: rotate(300deg); -moz-animation-timing-function: linear; -moz-origin: 0%; } 30% { opacity: 1; -moz-transform: rotate(410deg); -moz-animation-timing-function: ease-in-out; -moz-origin: 7%; } 39% { opacity: 1; -moz-transform: rotate(645deg); -moz-animation-timing-function: linear; -moz-origin: 30%; } 70% { opacity: 1; -moz-transform: rotate(770deg); -moz-animation-timing-function: ease-out; -moz-origin: 39%; } 75% { opacity: 1; -moz-transform: rotate(900deg); -moz-animation-timing-function: ease-out; -moz-origin: 70%; } 76% { opacity: 0; -moz-transform: rotate(900deg); } 100% { opacity: 0; -moz-transform: rotate(900deg); } } ================================================ FILE: src/electionguard_gui/web/index.html ================================================ <!DOCTYPE html> <html lang="en" dir="ltr"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <script type="text/javascript" src="/eel.js"></script> <link href="css/bootstrap.min.css" rel="stylesheet" /> <link href="css/bootstrap-overrides.css" rel="stylesheet" /> <link rel="stylesheet" href="css/bootstrap-icons.css" /> <link href="css/eg-styles.css" rel="stylesheet" /> <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" /> <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" /> <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" /> <link rel="manifest" href="/site.webmanifest" /> </head> <body> <script type="importmap"> { "imports": { "vue": "./js/vue.esm-browser.prod.js" } } </script> <div id="app"> <navbar :user-id="userId"></navbar> <div class="container mt-3 mb-4"> <Login @login="login" v-if="showLogin"></Login> <component :is="currentView" v-bind="currentViewProperties" /> </div> <eg-footer></eg-footer> </div> <script type="module"> import { createApp } from "vue"; // components import Navbar from "./components/shared/navbar-component.js"; import Login from "./components/shared/login-component.js"; import EgFooter from "./components/shared/footer-component.js"; // services import AuthorizationService from "./services/authorization-service.js"; import RouterService from "./services/router-service.js"; createApp({ data() { return { currentPath: window.location.hash, showLogin: false, userId: null, }; }, computed: { currentView() { if (this.showLogin) return null; const route = this.getRoute(this.currentPath); console.log("navigating", route); return route.component; }, currentViewProperties() { const querystringParams = this.currentPath.split("?")[1]; const urlSearchParams = new URLSearchParams(querystringParams); const params = Object.fromEntries(urlSearchParams.entries()); console.log("params", params); return params; }, }, methods: { getRoute(path) { return RouterService.getRoute(path); }, login(username) { console.log("login", username); this.showLogin = false; this.userId = username; }, }, async mounted() { window.addEventListener("hashchange", () => { // setting currentPath will trigger the computed properties to update this.currentPath = window.location.hash; const route = this.getRoute(this.currentPath); this.showLogin = route.secured && !this.userId; }); this.userId = await AuthorizationService.getUserId(); this.showLogin = !this.userId; }, components: { Navbar, Login, EgFooter, }, }).mount("#app"); </script> <script src="js/popper.min.js"></script> <script src="js/bootstrap.bundle.min.js"></script> </body> </html> ================================================ FILE: src/electionguard_gui/web/js/vue.esm-browser.prod.js ================================================ function e(e,t){const n=Object.create(null),o=e.split(",");for(let r=0;r<o.length;r++)n[o[r]]=!0;return t?e=>!!n[e.toLowerCase()]:e=>!!n[e]}const t=e("Infinity,undefined,NaN,isFinite,isNaN,parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt"),n=e("itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly");function o(e){return!!e||""===e}function r(e){if(E(e)){const t={};for(let n=0;n<e.length;n++){const o=e[n],s=P(o)?l(o):r(o);if(s)for(const e in s)t[e]=s[e]}return t}return P(e)||M(e)?e:void 0}const s=/;(?![^(]*\))/g,i=/:(.+)/;function l(e){const t={};return e.split(s).forEach((e=>{if(e){const n=e.split(i);n.length>1&&(t[n[0].trim()]=n[1].trim())}})),t}function c(e){let t="";if(P(e))t=e;else if(E(e))for(let n=0;n<e.length;n++){const o=c(e[n]);o&&(t+=o+" ")}else if(M(e))for(const n in e)e[n]&&(t+=n+" ");return t.trim()}function a(e){if(!e)return null;let{class:t,style:n}=e;return t&&!P(t)&&(e.class=c(t)),n&&(e.style=r(n)),e}const u=e("html,body,base,head,link,meta,style,title,address,article,aside,footer,header,h1,h2,h3,h4,h5,h6,nav,section,div,dd,dl,dt,figcaption,figure,picture,hr,img,li,main,ol,p,pre,ul,a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,ruby,s,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video,embed,object,param,source,canvas,script,noscript,del,ins,caption,col,colgroup,table,thead,tbody,td,th,tr,button,datalist,fieldset,form,input,label,legend,meter,optgroup,option,output,progress,select,textarea,details,dialog,menu,summary,template,blockquote,iframe,tfoot"),p=e("svg,animate,animateMotion,animateTransform,circle,clipPath,color-profile,defs,desc,discard,ellipse,feBlend,feColorMatrix,feComponentTransfer,feComposite,feConvolveMatrix,feDiffuseLighting,feDisplacementMap,feDistanceLight,feDropShadow,feFlood,feFuncA,feFuncB,feFuncG,feFuncR,feGaussianBlur,feImage,feMerge,feMergeNode,feMorphology,feOffset,fePointLight,feSpecularLighting,feSpotLight,feTile,feTurbulence,filter,foreignObject,g,hatch,hatchpath,image,line,linearGradient,marker,mask,mesh,meshgradient,meshpatch,meshrow,metadata,mpath,path,pattern,polygon,polyline,radialGradient,rect,set,solidcolor,stop,switch,symbol,text,textPath,title,tspan,unknown,use,view"),f=e("area,base,br,col,embed,hr,img,input,link,meta,param,source,track,wbr");function d(e,t){if(e===t)return!0;let n=R(e),o=R(t);if(n||o)return!(!n||!o)&&e.getTime()===t.getTime();if(n=A(e),o=A(t),n||o)return e===t;if(n=E(e),o=E(t),n||o)return!(!n||!o)&&function(e,t){if(e.length!==t.length)return!1;let n=!0;for(let o=0;n&&o<e.length;o++)n=d(e[o],t[o]);return n}(e,t);if(n=M(e),o=M(t),n||o){if(!n||!o)return!1;if(Object.keys(e).length!==Object.keys(t).length)return!1;for(const n in e){const o=e.hasOwnProperty(n),r=t.hasOwnProperty(n);if(o&&!r||!o&&r||!d(e[n],t[n]))return!1}}return String(e)===String(t)}function h(e,t){return e.findIndex((e=>d(e,t)))}const m=e=>P(e)?e:null==e?"":E(e)||M(e)&&(e.toString===I||!F(e.toString))?JSON.stringify(e,g,2):String(e),g=(e,t)=>t&&t.__v_isRef?g(e,t.value):$(t)?{[`Map(${t.size})`]:[...t.entries()].reduce(((e,[t,n])=>(e[`${t} =>`]=n,e)),{})}:O(t)?{[`Set(${t.size})`]:[...t.values()]}:!M(t)||E(t)||L(t)?t:String(t),v={},y=[],_=()=>{},b=()=>!1,S=/^on[^a-z]/,x=e=>S.test(e),C=e=>e.startsWith("onUpdate:"),w=Object.assign,k=(e,t)=>{const n=e.indexOf(t);n>-1&&e.splice(n,1)},T=Object.prototype.hasOwnProperty,N=(e,t)=>T.call(e,t),E=Array.isArray,$=e=>"[object Map]"===B(e),O=e=>"[object Set]"===B(e),R=e=>"[object Date]"===B(e),F=e=>"function"==typeof e,P=e=>"string"==typeof e,A=e=>"symbol"==typeof e,M=e=>null!==e&&"object"==typeof e,V=e=>M(e)&&F(e.then)&&F(e.catch),I=Object.prototype.toString,B=e=>I.call(e),L=e=>"[object Object]"===B(e),j=e=>P(e)&&"NaN"!==e&&"-"!==e[0]&&""+parseInt(e,10)===e,U=e(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),D=e("bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text,memo"),H=e=>{const t=Object.create(null);return n=>t[n]||(t[n]=e(n))},W=/-(\w)/g,z=H((e=>e.replace(W,((e,t)=>t?t.toUpperCase():"")))),K=/\B([A-Z])/g,G=H((e=>e.replace(K,"-$1").toLowerCase())),q=H((e=>e.charAt(0).toUpperCase()+e.slice(1))),J=H((e=>e?`on${q(e)}`:"")),Y=(e,t)=>!Object.is(e,t),Z=(e,t)=>{for(let n=0;n<e.length;n++)e[n](t)},Q=(e,t,n)=>{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,value:n})},X=e=>{const t=parseFloat(e);return isNaN(t)?e:t};let ee;let te;class ne{constructor(e=!1){this.active=!0,this.effects=[],this.cleanups=[],!e&&te&&(this.parent=te,this.index=(te.scopes||(te.scopes=[])).push(this)-1)}run(e){if(this.active){const t=te;try{return te=this,e()}finally{te=t}}}on(){te=this}off(){te=this.parent}stop(e){if(this.active){let t,n;for(t=0,n=this.effects.length;t<n;t++)this.effects[t].stop();for(t=0,n=this.cleanups.length;t<n;t++)this.cleanups[t]();if(this.scopes)for(t=0,n=this.scopes.length;t<n;t++)this.scopes[t].stop(!0);if(this.parent&&!e){const e=this.parent.scopes.pop();e&&e!==this&&(this.parent.scopes[this.index]=e,e.index=this.index)}this.active=!1}}}function oe(e){return new ne(e)}function re(e,t=te){t&&t.active&&t.effects.push(e)}function se(){return te}function ie(e){te&&te.cleanups.push(e)}const le=e=>{const t=new Set(e);return t.w=0,t.n=0,t},ce=e=>(e.w&fe)>0,ae=e=>(e.n&fe)>0,ue=new WeakMap;let pe=0,fe=1;let de;const he=Symbol(""),me=Symbol("");class ge{constructor(e,t=null,n){this.fn=e,this.scheduler=t,this.active=!0,this.deps=[],this.parent=void 0,re(this,n)}run(){if(!this.active)return this.fn();let e=de,t=be;for(;e;){if(e===this)return;e=e.parent}try{return this.parent=de,de=this,be=!0,fe=1<<++pe,pe<=30?(({deps:e})=>{if(e.length)for(let t=0;t<e.length;t++)e[t].w|=fe})(this):ve(this),this.fn()}finally{pe<=30&&(e=>{const{deps:t}=e;if(t.length){let n=0;for(let o=0;o<t.length;o++){const r=t[o];ce(r)&&!ae(r)?r.delete(e):t[n++]=r,r.w&=~fe,r.n&=~fe}t.length=n}})(this),fe=1<<--pe,de=this.parent,be=t,this.parent=void 0,this.deferStop&&this.stop()}}stop(){de===this?this.deferStop=!0:this.active&&(ve(this),this.onStop&&this.onStop(),this.active=!1)}}function ve(e){const{deps:t}=e;if(t.length){for(let n=0;n<t.length;n++)t[n].delete(e);t.length=0}}function ye(e,t){e.effect&&(e=e.effect.fn);const n=new ge(e);t&&(w(n,t),t.scope&&re(n,t.scope)),t&&t.lazy||n.run();const o=n.run.bind(n);return o.effect=n,o}function _e(e){e.effect.stop()}let be=!0;const Se=[];function xe(){Se.push(be),be=!1}function Ce(){const e=Se.pop();be=void 0===e||e}function we(e,t,n){if(be&&de){let t=ue.get(e);t||ue.set(e,t=new Map);let o=t.get(n);o||t.set(n,o=le()),ke(o)}}function ke(e,t){let n=!1;pe<=30?ae(e)||(e.n|=fe,n=!ce(e)):n=!e.has(de),n&&(e.add(de),de.deps.push(e))}function Te(e,t,n,o,r,s){const i=ue.get(e);if(!i)return;let l=[];if("clear"===t)l=[...i.values()];else if("length"===n&&E(e))i.forEach(((e,t)=>{("length"===t||t>=o)&&l.push(e)}));else switch(void 0!==n&&l.push(i.get(n)),t){case"add":E(e)?j(n)&&l.push(i.get("length")):(l.push(i.get(he)),$(e)&&l.push(i.get(me)));break;case"delete":E(e)||(l.push(i.get(he)),$(e)&&l.push(i.get(me)));break;case"set":$(e)&&l.push(i.get(he))}if(1===l.length)l[0]&&Ne(l[0]);else{const e=[];for(const t of l)t&&e.push(...t);Ne(le(e))}}function Ne(e,t){const n=E(e)?e:[...e];for(const o of n)o.computed&&Ee(o);for(const o of n)o.computed||Ee(o)}function Ee(e,t){(e!==de||e.allowRecurse)&&(e.scheduler?e.scheduler():e.run())}const $e=e("__proto__,__v_isRef,__isVue"),Oe=new Set(Object.getOwnPropertyNames(Symbol).filter((e=>"arguments"!==e&&"caller"!==e)).map((e=>Symbol[e])).filter(A)),Re=Ie(),Fe=Ie(!1,!0),Pe=Ie(!0),Ae=Ie(!0,!0),Me=Ve();function Ve(){const e={};return["includes","indexOf","lastIndexOf"].forEach((t=>{e[t]=function(...e){const n=kt(this);for(let t=0,r=this.length;t<r;t++)we(n,0,t+"");const o=n[t](...e);return-1===o||!1===o?n[t](...e.map(kt)):o}})),["push","pop","shift","unshift","splice"].forEach((t=>{e[t]=function(...e){xe();const n=kt(this)[t].apply(this,e);return Ce(),n}})),e}function Ie(e=!1,t=!1){return function(n,o,r){if("__v_isReactive"===o)return!e;if("__v_isReadonly"===o)return e;if("__v_isShallow"===o)return t;if("__v_raw"===o&&r===(e?t?ht:dt:t?ft:pt).get(n))return n;const s=E(n);if(!e&&s&&N(Me,o))return Reflect.get(Me,o,r);const i=Reflect.get(n,o,r);return(A(o)?Oe.has(o):$e(o))?i:(e||we(n,0,o),t?i:Rt(i)?s&&j(o)?i:i.value:M(i)?e?yt(i):gt(i):i)}}function Be(e=!1){return function(t,n,o,r){let s=t[n];if(xt(s)&&Rt(s)&&!Rt(o))return!1;if(!e&&!xt(o)&&(Ct(o)||(o=kt(o),s=kt(s)),!E(t)&&Rt(s)&&!Rt(o)))return s.value=o,!0;const i=E(t)&&j(n)?Number(n)<t.length:N(t,n),l=Reflect.set(t,n,o,r);return t===kt(r)&&(i?Y(o,s)&&Te(t,"set",n,o):Te(t,"add",n,o)),l}}const Le={get:Re,set:Be(),deleteProperty:function(e,t){const n=N(e,t),o=Reflect.deleteProperty(e,t);return o&&n&&Te(e,"delete",t,void 0),o},has:function(e,t){const n=Reflect.has(e,t);return A(t)&&Oe.has(t)||we(e,0,t),n},ownKeys:function(e){return we(e,0,E(e)?"length":he),Reflect.ownKeys(e)}},je={get:Pe,set:(e,t)=>!0,deleteProperty:(e,t)=>!0},Ue=w({},Le,{get:Fe,set:Be(!0)}),De=w({},je,{get:Ae}),He=e=>e,We=e=>Reflect.getPrototypeOf(e);function ze(e,t,n=!1,o=!1){const r=kt(e=e.__v_raw),s=kt(t);n||(t!==s&&we(r,0,t),we(r,0,s));const{has:i}=We(r),l=o?He:n?Et:Nt;return i.call(r,t)?l(e.get(t)):i.call(r,s)?l(e.get(s)):void(e!==r&&e.get(t))}function Ke(e,t=!1){const n=this.__v_raw,o=kt(n),r=kt(e);return t||(e!==r&&we(o,0,e),we(o,0,r)),e===r?n.has(e):n.has(e)||n.has(r)}function Ge(e,t=!1){return e=e.__v_raw,!t&&we(kt(e),0,he),Reflect.get(e,"size",e)}function qe(e){e=kt(e);const t=kt(this);return We(t).has.call(t,e)||(t.add(e),Te(t,"add",e,e)),this}function Je(e,t){t=kt(t);const n=kt(this),{has:o,get:r}=We(n);let s=o.call(n,e);s||(e=kt(e),s=o.call(n,e));const i=r.call(n,e);return n.set(e,t),s?Y(t,i)&&Te(n,"set",e,t):Te(n,"add",e,t),this}function Ye(e){const t=kt(this),{has:n,get:o}=We(t);let r=n.call(t,e);r||(e=kt(e),r=n.call(t,e)),o&&o.call(t,e);const s=t.delete(e);return r&&Te(t,"delete",e,void 0),s}function Ze(){const e=kt(this),t=0!==e.size,n=e.clear();return t&&Te(e,"clear",void 0,void 0),n}function Qe(e,t){return function(n,o){const r=this,s=r.__v_raw,i=kt(s),l=t?He:e?Et:Nt;return!e&&we(i,0,he),s.forEach(((e,t)=>n.call(o,l(e),l(t),r)))}}function Xe(e,t,n){return function(...o){const r=this.__v_raw,s=kt(r),i=$(s),l="entries"===e||e===Symbol.iterator&&i,c="keys"===e&&i,a=r[e](...o),u=n?He:t?Et:Nt;return!t&&we(s,0,c?me:he),{next(){const{value:e,done:t}=a.next();return t?{value:e,done:t}:{value:l?[u(e[0]),u(e[1])]:u(e),done:t}},[Symbol.iterator](){return this}}}}function et(e){return function(...t){return"delete"!==e&&this}}function tt(){const e={get(e){return ze(this,e)},get size(){return Ge(this)},has:Ke,add:qe,set:Je,delete:Ye,clear:Ze,forEach:Qe(!1,!1)},t={get(e){return ze(this,e,!1,!0)},get size(){return Ge(this)},has:Ke,add:qe,set:Je,delete:Ye,clear:Ze,forEach:Qe(!1,!0)},n={get(e){return ze(this,e,!0)},get size(){return Ge(this,!0)},has(e){return Ke.call(this,e,!0)},add:et("add"),set:et("set"),delete:et("delete"),clear:et("clear"),forEach:Qe(!0,!1)},o={get(e){return ze(this,e,!0,!0)},get size(){return Ge(this,!0)},has(e){return Ke.call(this,e,!0)},add:et("add"),set:et("set"),delete:et("delete"),clear:et("clear"),forEach:Qe(!0,!0)};return["keys","values","entries",Symbol.iterator].forEach((r=>{e[r]=Xe(r,!1,!1),n[r]=Xe(r,!0,!1),t[r]=Xe(r,!1,!0),o[r]=Xe(r,!0,!0)})),[e,n,t,o]}const[nt,ot,rt,st]=tt();function it(e,t){const n=t?e?st:rt:e?ot:nt;return(t,o,r)=>"__v_isReactive"===o?!e:"__v_isReadonly"===o?e:"__v_raw"===o?t:Reflect.get(N(n,o)&&o in t?n:t,o,r)}const lt={get:it(!1,!1)},ct={get:it(!1,!0)},at={get:it(!0,!1)},ut={get:it(!0,!0)},pt=new WeakMap,ft=new WeakMap,dt=new WeakMap,ht=new WeakMap;function mt(e){return e.__v_skip||!Object.isExtensible(e)?0:function(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}((e=>B(e).slice(8,-1))(e))}function gt(e){return xt(e)?e:bt(e,!1,Le,lt,pt)}function vt(e){return bt(e,!1,Ue,ct,ft)}function yt(e){return bt(e,!0,je,at,dt)}function _t(e){return bt(e,!0,De,ut,ht)}function bt(e,t,n,o,r){if(!M(e))return e;if(e.__v_raw&&(!t||!e.__v_isReactive))return e;const s=r.get(e);if(s)return s;const i=mt(e);if(0===i)return e;const l=new Proxy(e,2===i?o:n);return r.set(e,l),l}function St(e){return xt(e)?St(e.__v_raw):!(!e||!e.__v_isReactive)}function xt(e){return!(!e||!e.__v_isReadonly)}function Ct(e){return!(!e||!e.__v_isShallow)}function wt(e){return St(e)||xt(e)}function kt(e){const t=e&&e.__v_raw;return t?kt(t):e}function Tt(e){return Q(e,"__v_skip",!0),e}const Nt=e=>M(e)?gt(e):e,Et=e=>M(e)?yt(e):e;function $t(e){be&&de&&ke((e=kt(e)).dep||(e.dep=le()))}function Ot(e,t){(e=kt(e)).dep&&Ne(e.dep)}function Rt(e){return!(!e||!0!==e.__v_isRef)}function Ft(e){return At(e,!1)}function Pt(e){return At(e,!0)}function At(e,t){return Rt(e)?e:new Mt(e,t)}class Mt{constructor(e,t){this.__v_isShallow=t,this.dep=void 0,this.__v_isRef=!0,this._rawValue=t?e:kt(e),this._value=t?e:Nt(e)}get value(){return $t(this),this._value}set value(e){e=this.__v_isShallow?e:kt(e),Y(e,this._rawValue)&&(this._rawValue=e,this._value=this.__v_isShallow?e:Nt(e),Ot(this))}}function Vt(e){Ot(e)}function It(e){return Rt(e)?e.value:e}const Bt={get:(e,t,n)=>It(Reflect.get(e,t,n)),set:(e,t,n,o)=>{const r=e[t];return Rt(r)&&!Rt(n)?(r.value=n,!0):Reflect.set(e,t,n,o)}};function Lt(e){return St(e)?e:new Proxy(e,Bt)}class jt{constructor(e){this.dep=void 0,this.__v_isRef=!0;const{get:t,set:n}=e((()=>$t(this)),(()=>Ot(this)));this._get=t,this._set=n}get value(){return this._get()}set value(e){this._set(e)}}function Ut(e){return new jt(e)}function Dt(e){const t=E(e)?new Array(e.length):{};for(const n in e)t[n]=Wt(e,n);return t}class Ht{constructor(e,t,n){this._object=e,this._key=t,this._defaultValue=n,this.__v_isRef=!0}get value(){const e=this._object[this._key];return void 0===e?this._defaultValue:e}set value(e){this._object[this._key]=e}}function Wt(e,t,n){const o=e[t];return Rt(o)?o:new Ht(e,t,n)}class zt{constructor(e,t,n,o){this._setter=t,this.dep=void 0,this.__v_isRef=!0,this._dirty=!0,this.effect=new ge(e,(()=>{this._dirty||(this._dirty=!0,Ot(this))})),this.effect.computed=this,this.effect.active=this._cacheable=!o,this.__v_isReadonly=n}get value(){const e=kt(this);return $t(e),!e._dirty&&e._cacheable||(e._dirty=!1,e._value=e.effect.run()),e._value}set value(e){this._setter(e)}}const Kt=[];function Gt(e,...t){xe();const n=Kt.length?Kt[Kt.length-1].component:null,o=n&&n.appContext.config.warnHandler,r=function(){let e=Kt[Kt.length-1];if(!e)return[];const t=[];for(;e;){const n=t[0];n&&n.vnode===e?n.recurseCount++:t.push({vnode:e,recurseCount:0});const o=e.component&&e.component.parent;e=o&&o.vnode}return t}();if(o)Yt(o,n,11,[e+t.join(""),n&&n.proxy,r.map((({vnode:e})=>`at <${Ls(n,e.type)}>`)).join("\n"),r]);else{const n=[`[Vue warn]: ${e}`,...t];r.length&&n.push("\n",...function(e){const t=[];return e.forEach(((e,n)=>{t.push(...0===n?[]:["\n"],...function({vnode:e,recurseCount:t}){const n=t>0?`... (${t} recursive calls)`:"",o=` at <${Ls(e.component,e.type,!!e.component&&null==e.component.parent)}`,r=">"+n;return e.props?[o,...qt(e.props),r]:[o+r]}(e))})),t}(r)),console.warn(...n)}Ce()}function qt(e){const t=[],n=Object.keys(e);return n.slice(0,3).forEach((n=>{t.push(...Jt(n,e[n]))})),n.length>3&&t.push(" ..."),t}function Jt(e,t,n){return P(t)?(t=JSON.stringify(t),n?t:[`${e}=${t}`]):"number"==typeof t||"boolean"==typeof t||null==t?n?t:[`${e}=${t}`]:Rt(t)?(t=Jt(e,kt(t.value),!0),n?t:[`${e}=Ref<`,t,">"]):F(t)?[`${e}=fn${t.name?`<${t.name}>`:""}`]:(t=kt(t),n?t:[`${e}=`,t])}function Yt(e,t,n,o){let r;try{r=o?e(...o):e()}catch(s){Qt(s,t,n)}return r}function Zt(e,t,n,o){if(F(e)){const r=Yt(e,t,n,o);return r&&V(r)&&r.catch((e=>{Qt(e,t,n)})),r}const r=[];for(let s=0;s<e.length;s++)r.push(Zt(e[s],t,n,o));return r}function Qt(e,t,n,o=!0){if(t){let o=t.parent;const r=t.proxy,s=n;for(;o;){const t=o.ec;if(t)for(let n=0;n<t.length;n++)if(!1===t[n](e,r,s))return;o=o.parent}const i=t.appContext.config.errorHandler;if(i)return void Yt(i,null,10,[e,r,s])}!function(e,t,n,o=!0){console.error(e)}(e,0,0,o)}let Xt=!1,en=!1;const tn=[];let nn=0;const on=[];let rn=null,sn=0;const ln=[];let cn=null,an=0;const un=Promise.resolve();let pn=null,fn=null;function dn(e){const t=pn||un;return e?t.then(this?e.bind(this):e):t}function hn(e){tn.length&&tn.includes(e,Xt&&e.allowRecurse?nn+1:nn)||e===fn||(null==e.id?tn.push(e):tn.splice(function(e){let t=nn+1,n=tn.length;for(;t<n;){const o=t+n>>>1;bn(tn[o])<e?t=o+1:n=o}return t}(e.id),0,e),mn())}function mn(){Xt||en||(en=!0,pn=un.then(Sn))}function gn(e,t,n,o){E(e)?n.push(...e):t&&t.includes(e,e.allowRecurse?o+1:o)||n.push(e),mn()}function vn(e){gn(e,cn,ln,an)}function yn(e,t=null){if(on.length){for(fn=t,rn=[...new Set(on)],on.length=0,sn=0;sn<rn.length;sn++)rn[sn]();rn=null,sn=0,fn=null,yn(e,t)}}function _n(e){if(yn(),ln.length){const e=[...new Set(ln)];if(ln.length=0,cn)return void cn.push(...e);for(cn=e,cn.sort(((e,t)=>bn(e)-bn(t))),an=0;an<cn.length;an++)cn[an]();cn=null,an=0}}const bn=e=>null==e.id?1/0:e.id;function Sn(e){en=!1,Xt=!0,yn(e),tn.sort(((e,t)=>bn(e)-bn(t)));try{for(nn=0;nn<tn.length;nn++){const e=tn[nn];e&&!1!==e.active&&Yt(e,null,14)}}finally{nn=0,tn.length=0,_n(),Xt=!1,pn=null,(tn.length||on.length||ln.length)&&Sn(e)}}let xn,Cn=[];function wn(e,t){var n,o;if(xn=e,xn)xn.enabled=!0,Cn.forEach((({event:e,args:t})=>xn.emit(e,...t))),Cn=[];else if("undefined"!=typeof window&&window.HTMLElement&&!(null===(o=null===(n=window.navigator)||void 0===n?void 0:n.userAgent)||void 0===o?void 0:o.includes("jsdom"))){(t.__VUE_DEVTOOLS_HOOK_REPLAY__=t.__VUE_DEVTOOLS_HOOK_REPLAY__||[]).push((e=>{wn(e,t)})),setTimeout((()=>{xn||(t.__VUE_DEVTOOLS_HOOK_REPLAY__=null,Cn=[])}),3e3)}else Cn=[]}function kn(e,t,...n){if(e.isUnmounted)return;const o=e.vnode.props||v;let r=n;const s=t.startsWith("update:"),i=s&&t.slice(7);if(i&&i in o){const e=`${"modelValue"===i?"model":i}Modifiers`,{number:t,trim:s}=o[e]||v;s&&(r=n.map((e=>e.trim()))),t&&(r=n.map(X))}let l,c=o[l=J(t)]||o[l=J(z(t))];!c&&s&&(c=o[l=J(G(t))]),c&&Zt(c,e,6,r);const a=o[l+"Once"];if(a){if(e.emitted){if(e.emitted[l])return}else e.emitted={};e.emitted[l]=!0,Zt(a,e,6,r)}}function Tn(e,t,n=!1){const o=t.emitsCache,r=o.get(e);if(void 0!==r)return r;const s=e.emits;let i={},l=!1;if(!F(e)){const o=e=>{const n=Tn(e,t,!0);n&&(l=!0,w(i,n))};!n&&t.mixins.length&&t.mixins.forEach(o),e.extends&&o(e.extends),e.mixins&&e.mixins.forEach(o)}return s||l?(E(s)?s.forEach((e=>i[e]=null)):w(i,s),o.set(e,i),i):(o.set(e,null),null)}function Nn(e,t){return!(!e||!x(t))&&(t=t.slice(2).replace(/Once$/,""),N(e,t[0].toLowerCase()+t.slice(1))||N(e,G(t))||N(e,t))}let En=null,$n=null;function On(e){const t=En;return En=e,$n=e&&e.type.__scopeId||null,t}function Rn(e){$n=e}function Fn(){$n=null}const Pn=e=>An;function An(e,t=En,n){if(!t)return e;if(e._n)return e;const o=(...n)=>{o._d&&Xr(-1);const r=On(t),s=e(...n);return On(r),o._d&&Xr(1),s};return o._n=!0,o._c=!0,o._d=!0,o}function Mn(e){const{type:t,vnode:n,proxy:o,withProxy:r,props:s,propsOptions:[i],slots:l,attrs:c,emit:a,render:u,renderCache:p,data:f,setupState:d,ctx:h,inheritAttrs:m}=e;let g,v;const y=On(e);try{if(4&n.shapeFlag){const e=r||o;g=gs(u.call(e,e,p,s,d,f,h)),v=c}else{const e=t;0,g=gs(e(s,e.length>1?{attrs:c,slots:l,emit:a}:null)),v=t.props?c:Vn(c)}}catch(b){qr.length=0,Qt(b,e,1),g=us(Kr)}let _=g;if(v&&!1!==m){const e=Object.keys(v),{shapeFlag:t}=_;e.length&&7&t&&(i&&e.some(C)&&(v=In(v,i)),_=fs(_,v))}return n.dirs&&(_=fs(_),_.dirs=_.dirs?_.dirs.concat(n.dirs):n.dirs),n.transition&&(_.transition=n.transition),g=_,On(y),g}const Vn=e=>{let t;for(const n in e)("class"===n||"style"===n||x(n))&&((t||(t={}))[n]=e[n]);return t},In=(e,t)=>{const n={};for(const o in e)C(o)&&o.slice(9)in t||(n[o]=e[o]);return n};function Bn(e,t,n){const o=Object.keys(t);if(o.length!==Object.keys(e).length)return!0;for(let r=0;r<o.length;r++){const s=o[r];if(t[s]!==e[s]&&!Nn(n,s))return!0}return!1}function Ln({vnode:e,parent:t},n){for(;t&&t.subTree===e;)(e=t.vnode).el=n,t=t.parent}const jn=e=>e.__isSuspense,Un={name:"Suspense",__isSuspense:!0,process(e,t,n,o,r,s,i,l,c,a){null==e?function(e,t,n,o,r,s,i,l,c){const{p:a,o:{createElement:u}}=c,p=u("div"),f=e.suspense=Hn(e,r,o,t,p,n,s,i,l,c);a(null,f.pendingBranch=e.ssContent,p,null,o,f,s,i),f.deps>0?(Dn(e,"onPending"),Dn(e,"onFallback"),a(null,e.ssFallback,t,n,o,null,s,i),Kn(f,e.ssFallback)):f.resolve()}(t,n,o,r,s,i,l,c,a):function(e,t,n,o,r,s,i,l,{p:c,um:a,o:{createElement:u}}){const p=t.suspense=e.suspense;p.vnode=t,t.el=e.el;const f=t.ssContent,d=t.ssFallback,{activeBranch:h,pendingBranch:m,isInFallback:g,isHydrating:v}=p;if(m)p.pendingBranch=f,rs(f,m)?(c(m,f,p.hiddenContainer,null,r,p,s,i,l),p.deps<=0?p.resolve():g&&(c(h,d,n,o,r,null,s,i,l),Kn(p,d))):(p.pendingId++,v?(p.isHydrating=!1,p.activeBranch=m):a(m,r,p),p.deps=0,p.effects.length=0,p.hiddenContainer=u("div"),g?(c(null,f,p.hiddenContainer,null,r,p,s,i,l),p.deps<=0?p.resolve():(c(h,d,n,o,r,null,s,i,l),Kn(p,d))):h&&rs(f,h)?(c(h,f,n,o,r,p,s,i,l),p.resolve(!0)):(c(null,f,p.hiddenContainer,null,r,p,s,i,l),p.deps<=0&&p.resolve()));else if(h&&rs(f,h))c(h,f,n,o,r,p,s,i,l),Kn(p,f);else if(Dn(t,"onPending"),p.pendingBranch=f,p.pendingId++,c(null,f,p.hiddenContainer,null,r,p,s,i,l),p.deps<=0)p.resolve();else{const{timeout:e,pendingId:t}=p;e>0?setTimeout((()=>{p.pendingId===t&&p.fallback(d)}),e):0===e&&p.fallback(d)}}(e,t,n,o,r,i,l,c,a)},hydrate:function(e,t,n,o,r,s,i,l,c){const a=t.suspense=Hn(t,o,n,e.parentNode,document.createElement("div"),null,r,s,i,l,!0),u=c(e,a.pendingBranch=t.ssContent,n,a,s,i);0===a.deps&&a.resolve();return u},create:Hn,normalize:function(e){const{shapeFlag:t,children:n}=e,o=32&t;e.ssContent=Wn(o?n.default:n),e.ssFallback=o?Wn(n.fallback):us(Kr)}};function Dn(e,t){const n=e.props&&e.props[t];F(n)&&n()}function Hn(e,t,n,o,r,s,i,l,c,a,u=!1){const{p:p,m:f,um:d,n:h,o:{parentNode:m,remove:g}}=a,v=X(e.props&&e.props.timeout),y={vnode:e,parent:t,parentComponent:n,isSVG:i,container:o,hiddenContainer:r,anchor:s,deps:0,pendingId:0,timeout:"number"==typeof v?v:-1,activeBranch:null,pendingBranch:null,isInFallback:!0,isHydrating:u,isUnmounted:!1,effects:[],resolve(e=!1){const{vnode:t,activeBranch:n,pendingBranch:o,pendingId:r,effects:s,parentComponent:i,container:l}=y;if(y.isHydrating)y.isHydrating=!1;else if(!e){const e=n&&o.transition&&"out-in"===o.transition.mode;e&&(n.transition.afterLeave=()=>{r===y.pendingId&&f(o,l,t,0)});let{anchor:t}=y;n&&(t=h(n),d(n,i,y,!0)),e||f(o,l,t,0)}Kn(y,o),y.pendingBranch=null,y.isInFallback=!1;let c=y.parent,a=!1;for(;c;){if(c.pendingBranch){c.effects.push(...s),a=!0;break}c=c.parent}a||vn(s),y.effects=[],Dn(t,"onResolve")},fallback(e){if(!y.pendingBranch)return;const{vnode:t,activeBranch:n,parentComponent:o,container:r,isSVG:s}=y;Dn(t,"onFallback");const i=h(n),a=()=>{y.isInFallback&&(p(null,e,r,i,o,null,s,l,c),Kn(y,e))},u=e.transition&&"out-in"===e.transition.mode;u&&(n.transition.afterLeave=a),y.isInFallback=!0,d(n,o,null,!0),u||a()},move(e,t,n){y.activeBranch&&f(y.activeBranch,e,t,n),y.container=e},next:()=>y.activeBranch&&h(y.activeBranch),registerDep(e,t){const n=!!y.pendingBranch;n&&y.deps++;const o=e.vnode.el;e.asyncDep.catch((t=>{Qt(t,e,0)})).then((r=>{if(e.isUnmounted||y.isUnmounted||y.pendingId!==e.suspenseId)return;e.asyncResolved=!0;const{vnode:s}=e;Rs(e,r,!1),o&&(s.el=o);const l=!o&&e.subTree.el;t(e,s,m(o||e.subTree.el),o?null:h(e.subTree),y,i,c),l&&g(l),Ln(e,s.el),n&&0==--y.deps&&y.resolve()}))},unmount(e,t){y.isUnmounted=!0,y.activeBranch&&d(y.activeBranch,n,e,t),y.pendingBranch&&d(y.pendingBranch,n,e,t)}};return y}function Wn(e){let t;if(F(e)){const n=Qr&&e._c;n&&(e._d=!1,Yr()),e=e(),n&&(e._d=!0,t=Jr,Zr())}if(E(e)){const t=function(e){let t;for(let n=0;n<e.length;n++){const o=e[n];if(!os(o))return;if(o.type!==Kr||"v-if"===o.children){if(t)return;t=o}}return t}(e);e=t}return e=gs(e),t&&!e.dynamicChildren&&(e.dynamicChildren=t.filter((t=>t!==e))),e}function zn(e,t){t&&t.pendingBranch?E(e)?t.effects.push(...e):t.effects.push(e):vn(e)}function Kn(e,t){e.activeBranch=t;const{vnode:n,parentComponent:o}=e,r=n.el=t.el;o&&o.subTree===n&&(o.vnode.el=r,Ln(o,r))}function Gn(e,t){if(Cs){let n=Cs.provides;const o=Cs.parent&&Cs.parent.provides;o===n&&(n=Cs.provides=Object.create(o)),n[e]=t}else;}function qn(e,t,n=!1){const o=Cs||En;if(o){const r=null==o.parent?o.vnode.appContext&&o.vnode.appContext.provides:o.parent.provides;if(r&&e in r)return r[e];if(arguments.length>1)return n&&F(t)?t.call(o.proxy):t}}function Jn(e,t){return eo(e,null,t)}function Yn(e,t){return eo(e,null,{flush:"post"})}function Zn(e,t){return eo(e,null,{flush:"sync"})}const Qn={};function Xn(e,t,n){return eo(e,t,n)}function eo(e,t,{immediate:n,deep:o,flush:r}=v){const s=Cs;let i,l,c=!1,a=!1;if(Rt(e)?(i=()=>e.value,c=Ct(e)):St(e)?(i=()=>e,o=!0):E(e)?(a=!0,c=e.some((e=>St(e)||Ct(e))),i=()=>e.map((e=>Rt(e)?e.value:St(e)?oo(e):F(e)?Yt(e,s,2):void 0))):i=F(e)?t?()=>Yt(e,s,2):()=>{if(!s||!s.isUnmounted)return l&&l(),Zt(e,s,3,[u])}:_,t&&o){const e=i;i=()=>oo(e())}let u=e=>{l=h.onStop=()=>{Yt(e,s,4)}},p=a?[]:Qn;const f=()=>{if(h.active)if(t){const e=h.run();(o||c||(a?e.some(((e,t)=>Y(e,p[t]))):Y(e,p)))&&(l&&l(),Zt(t,s,3,[e,p===Qn?void 0:p,u]),p=e)}else h.run()};let d;f.allowRecurse=!!t,d="sync"===r?f:"post"===r?()=>Pr(f,s&&s.suspense):()=>function(e){gn(e,rn,on,sn)}(f);const h=new ge(i,d);return t?n?f():p=h.run():"post"===r?Pr(h.run.bind(h),s&&s.suspense):h.run(),()=>{h.stop(),s&&s.scope&&k(s.scope.effects,h)}}function to(e,t,n){const o=this.proxy,r=P(e)?e.includes(".")?no(o,e):()=>o[e]:e.bind(o,o);let s;F(t)?s=t:(s=t.handler,n=t);const i=Cs;ks(this);const l=eo(r,s.bind(o),n);return i?ks(i):Ts(),l}function no(e,t){const n=t.split(".");return()=>{let t=e;for(let e=0;e<n.length&&t;e++)t=t[n[e]];return t}}function oo(e,t){if(!M(e)||e.__v_skip)return e;if((t=t||new Set).has(e))return e;if(t.add(e),Rt(e))oo(e.value,t);else if(E(e))for(let n=0;n<e.length;n++)oo(e[n],t);else if(O(e)||$(e))e.forEach((e=>{oo(e,t)}));else if(L(e))for(const n in e)oo(e[n],t);return e}function ro(){const e={isMounted:!1,isLeaving:!1,isUnmounting:!1,leavingVNodes:new Map};return Oo((()=>{e.isMounted=!0})),Po((()=>{e.isUnmounting=!0})),e}const so=[Function,Array],io={name:"BaseTransition",props:{mode:String,appear:Boolean,persisted:Boolean,onBeforeEnter:so,onEnter:so,onAfterEnter:so,onEnterCancelled:so,onBeforeLeave:so,onLeave:so,onAfterLeave:so,onLeaveCancelled:so,onBeforeAppear:so,onAppear:so,onAfterAppear:so,onAppearCancelled:so},setup(e,{slots:t}){const n=ws(),o=ro();let r;return()=>{const s=t.default&&fo(t.default(),!0);if(!s||!s.length)return;let i=s[0];if(s.length>1)for(const e of s)if(e.type!==Kr){i=e;break}const l=kt(e),{mode:c}=l;if(o.isLeaving)return ao(i);const a=uo(i);if(!a)return ao(i);const u=co(a,l,o,n);po(a,u);const p=n.subTree,f=p&&uo(p);let d=!1;const{getTransitionKey:h}=a.type;if(h){const e=h();void 0===r?r=e:e!==r&&(r=e,d=!0)}if(f&&f.type!==Kr&&(!rs(a,f)||d)){const e=co(f,l,o,n);if(po(f,e),"out-in"===c)return o.isLeaving=!0,e.afterLeave=()=>{o.isLeaving=!1,n.update()},ao(i);"in-out"===c&&a.type!==Kr&&(e.delayLeave=(e,t,n)=>{lo(o,f)[String(f.key)]=f,e._leaveCb=()=>{t(),e._leaveCb=void 0,delete u.delayedLeave},u.delayedLeave=n})}return i}}};function lo(e,t){const{leavingVNodes:n}=e;let o=n.get(t.type);return o||(o=Object.create(null),n.set(t.type,o)),o}function co(e,t,n,o){const{appear:r,mode:s,persisted:i=!1,onBeforeEnter:l,onEnter:c,onAfterEnter:a,onEnterCancelled:u,onBeforeLeave:p,onLeave:f,onAfterLeave:d,onLeaveCancelled:h,onBeforeAppear:m,onAppear:g,onAfterAppear:v,onAppearCancelled:y}=t,_=String(e.key),b=lo(n,e),S=(e,t)=>{e&&Zt(e,o,9,t)},x=(e,t)=>{const n=t[1];S(e,t),E(e)?e.every((e=>e.length<=1))&&n():e.length<=1&&n()},C={mode:s,persisted:i,beforeEnter(t){let o=l;if(!n.isMounted){if(!r)return;o=m||l}t._leaveCb&&t._leaveCb(!0);const s=b[_];s&&rs(e,s)&&s.el._leaveCb&&s.el._leaveCb(),S(o,[t])},enter(e){let t=c,o=a,s=u;if(!n.isMounted){if(!r)return;t=g||c,o=v||a,s=y||u}let i=!1;const l=e._enterCb=t=>{i||(i=!0,S(t?s:o,[e]),C.delayedLeave&&C.delayedLeave(),e._enterCb=void 0)};t?x(t,[e,l]):l()},leave(t,o){const r=String(e.key);if(t._enterCb&&t._enterCb(!0),n.isUnmounting)return o();S(p,[t]);let s=!1;const i=t._leaveCb=n=>{s||(s=!0,o(),S(n?h:d,[t]),t._leaveCb=void 0,b[r]===e&&delete b[r])};b[r]=e,f?x(f,[t,i]):i()},clone:e=>co(e,t,n,o)};return C}function ao(e){if(yo(e))return(e=fs(e)).children=null,e}function uo(e){return yo(e)?e.children?e.children[0]:void 0:e}function po(e,t){6&e.shapeFlag&&e.component?po(e.component.subTree,t):128&e.shapeFlag?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function fo(e,t=!1,n){let o=[],r=0;for(let s=0;s<e.length;s++){let i=e[s];const l=null==n?i.key:String(n)+String(null!=i.key?i.key:s);i.type===Wr?(128&i.patchFlag&&r++,o=o.concat(fo(i.children,t,l))):(t||i.type!==Kr)&&o.push(null!=l?fs(i,{key:l}):i)}if(r>1)for(let s=0;s<o.length;s++)o[s].patchFlag=-2;return o}function ho(e){return F(e)?{setup:e,name:e.name}:e}const mo=e=>!!e.type.__asyncLoader;function go(e){F(e)&&(e={loader:e});const{loader:t,loadingComponent:n,errorComponent:o,delay:r=200,timeout:s,suspensible:i=!0,onError:l}=e;let c,a=null,u=0;const p=()=>{let e;return a||(e=a=t().catch((e=>{if(e=e instanceof Error?e:new Error(String(e)),l)return new Promise(((t,n)=>{l(e,(()=>t((u++,a=null,p()))),(()=>n(e)),u+1)}));throw e})).then((t=>e!==a&&a?a:(t&&(t.__esModule||"Module"===t[Symbol.toStringTag])&&(t=t.default),c=t,t))))};return ho({name:"AsyncComponentWrapper",__asyncLoader:p,get __asyncResolved(){return c},setup(){const e=Cs;if(c)return()=>vo(c,e);const t=t=>{a=null,Qt(t,e,13,!o)};if(i&&e.suspense)return p().then((t=>()=>vo(t,e))).catch((e=>(t(e),()=>o?us(o,{error:e}):null)));const l=Ft(!1),u=Ft(),f=Ft(!!r);return r&&setTimeout((()=>{f.value=!1}),r),null!=s&&setTimeout((()=>{if(!l.value&&!u.value){const e=new Error(`Async component timed out after ${s}ms.`);t(e),u.value=e}}),s),p().then((()=>{l.value=!0,e.parent&&yo(e.parent.vnode)&&hn(e.parent.update)})).catch((e=>{t(e),u.value=e})),()=>l.value&&c?vo(c,e):u.value&&o?us(o,{error:u.value}):n&&!f.value?us(n):void 0}})}function vo(e,{vnode:{ref:t,props:n,children:o}}){const r=us(e,n,o);return r.ref=t,r}const yo=e=>e.type.__isKeepAlive,_o={name:"KeepAlive",__isKeepAlive:!0,props:{include:[String,RegExp,Array],exclude:[String,RegExp,Array],max:[String,Number]},setup(e,{slots:t}){const n=ws(),o=n.ctx,r=new Map,s=new Set;let i=null;const l=n.suspense,{renderer:{p:c,m:a,um:u,o:{createElement:p}}}=o,f=p("div");function d(e){ko(e),u(e,n,l,!0)}function h(e){r.forEach(((t,n)=>{const o=Bs(t.type);!o||e&&e(o)||m(n)}))}function m(e){const t=r.get(e);i&&t.type===i.type?i&&ko(i):d(t),r.delete(e),s.delete(e)}o.activate=(e,t,n,o,r)=>{const s=e.component;a(e,t,n,0,l),c(s.vnode,e,t,n,s,l,o,e.slotScopeIds,r),Pr((()=>{s.isDeactivated=!1,s.a&&Z(s.a);const t=e.props&&e.props.onVnodeMounted;t&&bs(t,s.parent,e)}),l)},o.deactivate=e=>{const t=e.component;a(e,f,null,1,l),Pr((()=>{t.da&&Z(t.da);const n=e.props&&e.props.onVnodeUnmounted;n&&bs(n,t.parent,e),t.isDeactivated=!0}),l)},Xn((()=>[e.include,e.exclude]),(([e,t])=>{e&&h((t=>bo(e,t))),t&&h((e=>!bo(t,e)))}),{flush:"post",deep:!0});let g=null;const v=()=>{null!=g&&r.set(g,To(n.subTree))};return Oo(v),Fo(v),Po((()=>{r.forEach((e=>{const{subTree:t,suspense:o}=n,r=To(t);if(e.type!==r.type)d(e);else{ko(r);const e=r.component.da;e&&Pr(e,o)}}))})),()=>{if(g=null,!t.default)return null;const n=t.default(),o=n[0];if(n.length>1)return i=null,n;if(!(os(o)&&(4&o.shapeFlag||128&o.shapeFlag)))return i=null,o;let l=To(o);const c=l.type,a=Bs(mo(l)?l.type.__asyncResolved||{}:c),{include:u,exclude:p,max:f}=e;if(u&&(!a||!bo(u,a))||p&&a&&bo(p,a))return i=l,o;const d=null==l.key?c:l.key,h=r.get(d);return l.el&&(l=fs(l),128&o.shapeFlag&&(o.ssContent=l)),g=d,h?(l.el=h.el,l.component=h.component,l.transition&&po(l,l.transition),l.shapeFlag|=512,s.delete(d),s.add(d)):(s.add(d),f&&s.size>parseInt(f,10)&&m(s.values().next().value)),l.shapeFlag|=256,i=l,jn(o.type)?o:l}}};function bo(e,t){return E(e)?e.some((e=>bo(e,t))):P(e)?e.split(",").includes(t):!!e.test&&e.test(t)}function So(e,t){Co(e,"a",t)}function xo(e,t){Co(e,"da",t)}function Co(e,t,n=Cs){const o=e.__wdc||(e.__wdc=()=>{let t=n;for(;t;){if(t.isDeactivated)return;t=t.parent}return e()});if(No(t,o,n),n){let e=n.parent;for(;e&&e.parent;)yo(e.parent.vnode)&&wo(o,t,n,e),e=e.parent}}function wo(e,t,n,o){const r=No(t,e,o,!0);Ao((()=>{k(o[t],r)}),n)}function ko(e){let t=e.shapeFlag;256&t&&(t-=256),512&t&&(t-=512),e.shapeFlag=t}function To(e){return 128&e.shapeFlag?e.ssContent:e}function No(e,t,n=Cs,o=!1){if(n){const r=n[e]||(n[e]=[]),s=t.__weh||(t.__weh=(...o)=>{if(n.isUnmounted)return;xe(),ks(n);const r=Zt(t,n,e,o);return Ts(),Ce(),r});return o?r.unshift(s):r.push(s),s}}const Eo=e=>(t,n=Cs)=>(!Os||"sp"===e)&&No(e,t,n),$o=Eo("bm"),Oo=Eo("m"),Ro=Eo("bu"),Fo=Eo("u"),Po=Eo("bum"),Ao=Eo("um"),Mo=Eo("sp"),Vo=Eo("rtg"),Io=Eo("rtc");function Bo(e,t=Cs){No("ec",e,t)}function Lo(e,t){const n=En;if(null===n)return e;const o=Vs(n)||n.proxy,r=e.dirs||(e.dirs=[]);for(let s=0;s<t.length;s++){let[e,n,i,l=v]=t[s];F(e)&&(e={mounted:e,updated:e}),e.deep&&oo(n),r.push({dir:e,instance:o,value:n,oldValue:void 0,arg:i,modifiers:l})}return e}function jo(e,t,n,o){const r=e.dirs,s=t&&t.dirs;for(let i=0;i<r.length;i++){const l=r[i];s&&(l.oldValue=s[i].value);let c=l.dir[o];c&&(xe(),Zt(c,n,8,[e.el,l,e,t]),Ce())}}function Uo(e,t){return zo("components",e,!0,t)||e}const Do=Symbol();function Ho(e){return P(e)?zo("components",e,!1)||e:e||Do}function Wo(e){return zo("directives",e)}function zo(e,t,n=!0,o=!1){const r=En||Cs;if(r){const n=r.type;if("components"===e){const e=Bs(n,!1);if(e&&(e===t||e===z(t)||e===q(z(t))))return n}const s=Ko(r[e]||n[e],t)||Ko(r.appContext[e],t);return!s&&o?n:s}}function Ko(e,t){return e&&(e[t]||e[z(t)]||e[q(z(t))])}function Go(e,t,n,o){let r;const s=n&&n[o];if(E(e)||P(e)){r=new Array(e.length);for(let n=0,o=e.length;n<o;n++)r[n]=t(e[n],n,void 0,s&&s[n])}else if("number"==typeof e){r=new Array(e);for(let n=0;n<e;n++)r[n]=t(n+1,n,void 0,s&&s[n])}else if(M(e))if(e[Symbol.iterator])r=Array.from(e,((e,n)=>t(e,n,void 0,s&&s[n])));else{const n=Object.keys(e);r=new Array(n.length);for(let o=0,i=n.length;o<i;o++){const i=n[o];r[o]=t(e[i],i,o,s&&s[o])}}else r=[];return n&&(n[o]=r),r}function qo(e,t){for(let n=0;n<t.length;n++){const o=t[n];if(E(o))for(let t=0;t<o.length;t++)e[o[t].name]=o[t].fn;else o&&(e[o.name]=o.fn)}return e}function Jo(e,t,n={},o,r){if(En.isCE||En.parent&&mo(En.parent)&&En.parent.isCE)return us("slot","default"===t?null:{name:t},o&&o());let s=e[t];s&&s._c&&(s._d=!1),Yr();const i=s&&Yo(s(n)),l=ns(Wr,{key:n.key||`_${t}`},i||(o?o():[]),i&&1===e._?64:-2);return!r&&l.scopeId&&(l.slotScopeIds=[l.scopeId+"-s"]),s&&s._c&&(s._d=!0),l}function Yo(e){return e.some((e=>!os(e)||e.type!==Kr&&!(e.type===Wr&&!Yo(e.children))))?e:null}function Zo(e){const t={};for(const n in e)t[J(n)]=e[n];return t}const Qo=e=>e?Ns(e)?Vs(e)||e.proxy:Qo(e.parent):null,Xo=w(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>Qo(e.parent),$root:e=>Qo(e.root),$emit:e=>e.emit,$options:e=>ir(e),$forceUpdate:e=>e.f||(e.f=()=>hn(e.update)),$nextTick:e=>e.n||(e.n=dn.bind(e.proxy)),$watch:e=>to.bind(e)}),er={get({_:e},t){const{ctx:n,setupState:o,data:r,props:s,accessCache:i,type:l,appContext:c}=e;let a;if("$"!==t[0]){const l=i[t];if(void 0!==l)switch(l){case 1:return o[t];case 2:return r[t];case 4:return n[t];case 3:return s[t]}else{if(o!==v&&N(o,t))return i[t]=1,o[t];if(r!==v&&N(r,t))return i[t]=2,r[t];if((a=e.propsOptions[0])&&N(a,t))return i[t]=3,s[t];if(n!==v&&N(n,t))return i[t]=4,n[t];nr&&(i[t]=0)}}const u=Xo[t];let p,f;return u?("$attrs"===t&&we(e,0,t),u(e)):(p=l.__cssModules)&&(p=p[t])?p:n!==v&&N(n,t)?(i[t]=4,n[t]):(f=c.config.globalProperties,N(f,t)?f[t]:void 0)},set({_:e},t,n){const{data:o,setupState:r,ctx:s}=e;return r!==v&&N(r,t)?(r[t]=n,!0):o!==v&&N(o,t)?(o[t]=n,!0):!N(e.props,t)&&(("$"!==t[0]||!(t.slice(1)in e))&&(s[t]=n,!0))},has({_:{data:e,setupState:t,accessCache:n,ctx:o,appContext:r,propsOptions:s}},i){let l;return!!n[i]||e!==v&&N(e,i)||t!==v&&N(t,i)||(l=s[0])&&N(l,i)||N(o,i)||N(Xo,i)||N(r.config.globalProperties,i)},defineProperty(e,t,n){return null!=n.get?e._.accessCache[t]=0:N(n,"value")&&this.set(e,t,n.value,null),Reflect.defineProperty(e,t,n)}},tr=w({},er,{get(e,t){if(t!==Symbol.unscopables)return er.get(e,t,e)},has:(e,n)=>"_"!==n[0]&&!t(n)});let nr=!0;function or(e){const t=ir(e),n=e.proxy,o=e.ctx;nr=!1,t.beforeCreate&&rr(t.beforeCreate,e,"bc");const{data:r,computed:s,methods:i,watch:l,provide:c,inject:a,created:u,beforeMount:p,mounted:f,beforeUpdate:d,updated:h,activated:m,deactivated:g,beforeUnmount:v,unmounted:y,render:b,renderTracked:S,renderTriggered:x,errorCaptured:C,serverPrefetch:w,expose:k,inheritAttrs:T,components:N,directives:$}=t;if(a&&function(e,t,n=_,o=!1){E(e)&&(e=ur(e));for(const r in e){const n=e[r];let s;s=M(n)?"default"in n?qn(n.from||r,n.default,!0):qn(n.from||r):qn(n),Rt(s)&&o?Object.defineProperty(t,r,{enumerable:!0,configurable:!0,get:()=>s.value,set:e=>s.value=e}):t[r]=s}}(a,o,null,e.appContext.config.unwrapInjectedRef),i)for(const _ in i){const e=i[_];F(e)&&(o[_]=e.bind(n))}if(r){const t=r.call(n,n);M(t)&&(e.data=gt(t))}if(nr=!0,s)for(const E in s){const e=s[E],t=F(e)?e.bind(n,n):F(e.get)?e.get.bind(n,n):_,r=!F(e)&&F(e.set)?e.set.bind(n):_,i=js({get:t,set:r});Object.defineProperty(o,E,{enumerable:!0,configurable:!0,get:()=>i.value,set:e=>i.value=e})}if(l)for(const _ in l)sr(l[_],o,n,_);if(c){const e=F(c)?c.call(n):c;Reflect.ownKeys(e).forEach((t=>{Gn(t,e[t])}))}function O(e,t){E(t)?t.forEach((t=>e(t.bind(n)))):t&&e(t.bind(n))}if(u&&rr(u,e,"c"),O($o,p),O(Oo,f),O(Ro,d),O(Fo,h),O(So,m),O(xo,g),O(Bo,C),O(Io,S),O(Vo,x),O(Po,v),O(Ao,y),O(Mo,w),E(k))if(k.length){const t=e.exposed||(e.exposed={});k.forEach((e=>{Object.defineProperty(t,e,{get:()=>n[e],set:t=>n[e]=t})}))}else e.exposed||(e.exposed={});b&&e.render===_&&(e.render=b),null!=T&&(e.inheritAttrs=T),N&&(e.components=N),$&&(e.directives=$)}function rr(e,t,n){Zt(E(e)?e.map((e=>e.bind(t.proxy))):e.bind(t.proxy),t,n)}function sr(e,t,n,o){const r=o.includes(".")?no(n,o):()=>n[o];if(P(e)){const n=t[e];F(n)&&Xn(r,n)}else if(F(e))Xn(r,e.bind(n));else if(M(e))if(E(e))e.forEach((e=>sr(e,t,n,o)));else{const o=F(e.handler)?e.handler.bind(n):t[e.handler];F(o)&&Xn(r,o,e)}}function ir(e){const t=e.type,{mixins:n,extends:o}=t,{mixins:r,optionsCache:s,config:{optionMergeStrategies:i}}=e.appContext,l=s.get(t);let c;return l?c=l:r.length||n||o?(c={},r.length&&r.forEach((e=>lr(c,e,i,!0))),lr(c,t,i)):c=t,s.set(t,c),c}function lr(e,t,n,o=!1){const{mixins:r,extends:s}=t;s&&lr(e,s,n,!0),r&&r.forEach((t=>lr(e,t,n,!0)));for(const i in t)if(o&&"expose"===i);else{const o=cr[i]||n&&n[i];e[i]=o?o(e[i],t[i]):t[i]}return e}const cr={data:ar,props:fr,emits:fr,methods:fr,computed:fr,beforeCreate:pr,created:pr,beforeMount:pr,mounted:pr,beforeUpdate:pr,updated:pr,beforeDestroy:pr,beforeUnmount:pr,destroyed:pr,unmounted:pr,activated:pr,deactivated:pr,errorCaptured:pr,serverPrefetch:pr,components:fr,directives:fr,watch:function(e,t){if(!e)return t;if(!t)return e;const n=w(Object.create(null),e);for(const o in t)n[o]=pr(e[o],t[o]);return n},provide:ar,inject:function(e,t){return fr(ur(e),ur(t))}};function ar(e,t){return t?e?function(){return w(F(e)?e.call(this,this):e,F(t)?t.call(this,this):t)}:t:e}function ur(e){if(E(e)){const t={};for(let n=0;n<e.length;n++)t[e[n]]=e[n];return t}return e}function pr(e,t){return e?[...new Set([].concat(e,t))]:t}function fr(e,t){return e?w(w(Object.create(null),e),t):t}function dr(e,t,n,o){const[r,s]=e.propsOptions;let i,l=!1;if(t)for(let c in t){if(U(c))continue;const a=t[c];let u;r&&N(r,u=z(c))?s&&s.includes(u)?(i||(i={}))[u]=a:n[u]=a:Nn(e.emitsOptions,c)||c in o&&a===o[c]||(o[c]=a,l=!0)}if(s){const t=kt(n),o=i||v;for(let i=0;i<s.length;i++){const l=s[i];n[l]=hr(r,t,l,o[l],e,!N(o,l))}}return l}function hr(e,t,n,o,r,s){const i=e[n];if(null!=i){const e=N(i,"default");if(e&&void 0===o){const e=i.default;if(i.type!==Function&&F(e)){const{propsDefaults:s}=r;n in s?o=s[n]:(ks(r),o=s[n]=e.call(null,t),Ts())}else o=e}i[0]&&(s&&!e?o=!1:!i[1]||""!==o&&o!==G(n)||(o=!0))}return o}function mr(e,t,n=!1){const o=t.propsCache,r=o.get(e);if(r)return r;const s=e.props,i={},l=[];let c=!1;if(!F(e)){const o=e=>{c=!0;const[n,o]=mr(e,t,!0);w(i,n),o&&l.push(...o)};!n&&t.mixins.length&&t.mixins.forEach(o),e.extends&&o(e.extends),e.mixins&&e.mixins.forEach(o)}if(!s&&!c)return o.set(e,y),y;if(E(s))for(let u=0;u<s.length;u++){const e=z(s[u]);gr(e)&&(i[e]=v)}else if(s)for(const u in s){const e=z(u);if(gr(e)){const t=s[u],n=i[e]=E(t)||F(t)?{type:t}:t;if(n){const t=_r(Boolean,n.type),o=_r(String,n.type);n[0]=t>-1,n[1]=o<0||t<o,(t>-1||N(n,"default"))&&l.push(e)}}}const a=[i,l];return o.set(e,a),a}function gr(e){return"$"!==e[0]}function vr(e){const t=e&&e.toString().match(/^\s*function (\w+)/);return t?t[1]:null===e?"null":""}function yr(e,t){return vr(e)===vr(t)}function _r(e,t){return E(t)?t.findIndex((t=>yr(t,e))):F(t)&&yr(t,e)?0:-1}const br=e=>"_"===e[0]||"$stable"===e,Sr=e=>E(e)?e.map(gs):[gs(e)],xr=(e,t,n)=>{if(t._n)return t;const o=An(((...e)=>Sr(t(...e))),n);return o._c=!1,o},Cr=(e,t,n)=>{const o=e._ctx;for(const r in e){if(br(r))continue;const n=e[r];if(F(n))t[r]=xr(0,n,o);else if(null!=n){const e=Sr(n);t[r]=()=>e}}},wr=(e,t)=>{const n=Sr(t);e.slots.default=()=>n};function kr(){return{app:null,config:{isNativeTag:b,performance:!1,globalProperties:{},optionMergeStrategies:{},errorHandler:void 0,warnHandler:void 0,compilerOptions:{}},mixins:[],components:{},directives:{},provides:Object.create(null),optionsCache:new WeakMap,propsCache:new WeakMap,emitsCache:new WeakMap}}let Tr=0;function Nr(e,t){return function(n,o=null){F(n)||(n=Object.assign({},n)),null==o||M(o)||(o=null);const r=kr(),s=new Set;let i=!1;const l=r.app={_uid:Tr++,_component:n,_props:o,_container:null,_context:r,_instance:null,version:oi,get config(){return r.config},set config(e){},use:(e,...t)=>(s.has(e)||(e&&F(e.install)?(s.add(e),e.install(l,...t)):F(e)&&(s.add(e),e(l,...t))),l),mixin:e=>(r.mixins.includes(e)||r.mixins.push(e),l),component:(e,t)=>t?(r.components[e]=t,l):r.components[e],directive:(e,t)=>t?(r.directives[e]=t,l):r.directives[e],mount(s,c,a){if(!i){const u=us(n,o);return u.appContext=r,c&&t?t(u,s):e(u,s,a),i=!0,l._container=s,s.__vue_app__=l,Vs(u.component)||u.component.proxy}},unmount(){i&&(e(null,l._container),delete l._container.__vue_app__)},provide:(e,t)=>(r.provides[e]=t,l)};return l}}function Er(e,t,n,o,r=!1){if(E(e))return void e.forEach(((e,s)=>Er(e,t&&(E(t)?t[s]:t),n,o,r)));if(mo(o)&&!r)return;const s=4&o.shapeFlag?Vs(o.component)||o.component.proxy:o.el,i=r?null:s,{i:l,r:c}=e,a=t&&t.r,u=l.refs===v?l.refs={}:l.refs,p=l.setupState;if(null!=a&&a!==c&&(P(a)?(u[a]=null,N(p,a)&&(p[a]=null)):Rt(a)&&(a.value=null)),F(c))Yt(c,l,12,[i,u]);else{const t=P(c),o=Rt(c);if(t||o){const l=()=>{if(e.f){const n=t?u[c]:c.value;r?E(n)&&k(n,s):E(n)?n.includes(s)||n.push(s):t?(u[c]=[s],N(p,c)&&(p[c]=u[c])):(c.value=[s],e.k&&(u[e.k]=c.value))}else t?(u[c]=i,N(p,c)&&(p[c]=i)):o&&(c.value=i,e.k&&(u[e.k]=i))};i?(l.id=-1,Pr(l,n)):l()}}}let $r=!1;const Or=e=>/svg/.test(e.namespaceURI)&&"foreignObject"!==e.tagName,Rr=e=>8===e.nodeType;function Fr(e){const{mt:t,p:n,o:{patchProp:o,createText:r,nextSibling:s,parentNode:i,remove:l,insert:c,createComment:a}}=e,u=(n,o,l,a,g,v=!1)=>{const y=Rr(n)&&"["===n.data,_=()=>h(n,o,l,a,g,y),{type:b,ref:S,shapeFlag:x,patchFlag:C}=o,w=n.nodeType;o.el=n,-2===C&&(v=!1,o.dynamicChildren=null);let k=null;switch(b){case zr:3!==w?""===o.children?(c(o.el=r(""),i(n),n),k=n):k=_():(n.data!==o.children&&($r=!0,n.data=o.children),k=s(n));break;case Kr:k=8!==w||y?_():s(n);break;case Gr:if(1===w||3===w){k=n;const e=!o.children.length;for(let t=0;t<o.staticCount;t++)e&&(o.children+=1===k.nodeType?k.outerHTML:k.data),t===o.staticCount-1&&(o.anchor=k),k=s(k);return k}k=_();break;case Wr:k=y?d(n,o,l,a,g,v):_();break;default:if(1&x)k=1!==w||o.type.toLowerCase()!==n.tagName.toLowerCase()?_():p(n,o,l,a,g,v);else if(6&x){o.slotScopeIds=g;const e=i(n);if(t(o,e,null,l,a,Or(e),v),k=y?m(n):s(n),k&&Rr(k)&&"teleport end"===k.data&&(k=s(k)),mo(o)){let t;y?(t=us(Wr),t.anchor=k?k.previousSibling:e.lastChild):t=3===n.nodeType?ds(""):us("div"),t.el=n,o.component.subTree=t}}else 64&x?k=8!==w?_():o.type.hydrate(n,o,l,a,g,v,e,f):128&x&&(k=o.type.hydrate(n,o,l,a,Or(i(n)),g,v,e,u))}return null!=S&&Er(S,null,a,o),k},p=(e,t,n,r,s,i)=>{i=i||!!t.dynamicChildren;const{type:c,props:a,patchFlag:u,shapeFlag:p,dirs:d}=t,h="input"===c&&d||"option"===c;if(h||-1!==u){if(d&&jo(t,null,n,"created"),a)if(h||!i||48&u)for(const t in a)(h&&t.endsWith("value")||x(t)&&!U(t))&&o(e,t,null,a[t],!1,void 0,n);else a.onClick&&o(e,"onClick",null,a.onClick,!1,void 0,n);let c;if((c=a&&a.onVnodeBeforeMount)&&bs(c,n,t),d&&jo(t,null,n,"beforeMount"),((c=a&&a.onVnodeMounted)||d)&&zn((()=>{c&&bs(c,n,t),d&&jo(t,null,n,"mounted")}),r),16&p&&(!a||!a.innerHTML&&!a.textContent)){let o=f(e.firstChild,t,e,n,r,s,i);for(;o;){$r=!0;const e=o;o=o.nextSibling,l(e)}}else 8&p&&e.textContent!==t.children&&($r=!0,e.textContent=t.children)}return e.nextSibling},f=(e,t,o,r,s,i,l)=>{l=l||!!t.dynamicChildren;const c=t.children,a=c.length;for(let p=0;p<a;p++){const t=l?c[p]:c[p]=gs(c[p]);if(e)e=u(e,t,r,s,i,l);else{if(t.type===zr&&!t.children)continue;$r=!0,n(null,t,o,null,r,s,Or(o),i)}}return e},d=(e,t,n,o,r,l)=>{const{slotScopeIds:u}=t;u&&(r=r?r.concat(u):u);const p=i(e),d=f(s(e),t,p,n,o,r,l);return d&&Rr(d)&&"]"===d.data?s(t.anchor=d):($r=!0,c(t.anchor=a("]"),p,d),d)},h=(e,t,o,r,c,a)=>{if($r=!0,t.el=null,a){const t=m(e);for(;;){const n=s(e);if(!n||n===t)break;l(n)}}const u=s(e),p=i(e);return l(e),n(null,t,p,u,o,r,Or(p),c),u},m=e=>{let t=0;for(;e;)if((e=s(e))&&Rr(e)&&("["===e.data&&t++,"]"===e.data)){if(0===t)return s(e);t--}return e};return[(e,t)=>{if(!t.hasChildNodes())return n(null,e,t),_n(),void(t._vnode=e);$r=!1,u(t.firstChild,e,null,null,null),_n(),t._vnode=e,$r&&console.error("Hydration completed but contains mismatches.")},u]}const Pr=zn;function Ar(e){return Vr(e)}function Mr(e){return Vr(e,Fr)}function Vr(e,t){(ee||(ee="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:{})).__VUE__=!0;const{insert:n,remove:o,patchProp:r,createElement:s,createText:i,createComment:l,setText:c,setElementText:a,parentNode:u,nextSibling:p,setScopeId:f=_,cloneNode:d,insertStaticContent:h}=e,m=(e,t,n,o=null,r=null,s=null,i=!1,l=null,c=!!t.dynamicChildren)=>{if(e===t)return;e&&!rs(e,t)&&(o=Y(e),H(e,r,s,!0),e=null),-2===t.patchFlag&&(c=!1,t.dynamicChildren=null);const{type:a,ref:u,shapeFlag:p}=t;switch(a){case zr:g(e,t,n,o);break;case Kr:b(e,t,n,o);break;case Gr:null==e&&S(t,n,o,i);break;case Wr:R(e,t,n,o,r,s,i,l,c);break;default:1&p?x(e,t,n,o,r,s,i,l,c):6&p?F(e,t,n,o,r,s,i,l,c):(64&p||128&p)&&a.process(e,t,n,o,r,s,i,l,c,te)}null!=u&&r&&Er(u,e&&e.ref,s,t||e,!t)},g=(e,t,o,r)=>{if(null==e)n(t.el=i(t.children),o,r);else{const n=t.el=e.el;t.children!==e.children&&c(n,t.children)}},b=(e,t,o,r)=>{null==e?n(t.el=l(t.children||""),o,r):t.el=e.el},S=(e,t,n,o)=>{[e.el,e.anchor]=h(e.children,t,n,o,e.el,e.anchor)},x=(e,t,n,o,r,s,i,l,c)=>{i=i||"svg"===t.type,null==e?C(t,n,o,r,s,i,l,c):E(e,t,r,s,i,l,c)},C=(e,t,o,i,l,c,u,p)=>{let f,h;const{type:m,props:g,shapeFlag:v,transition:y,patchFlag:_,dirs:b}=e;if(e.el&&void 0!==d&&-1===_)f=e.el=d(e.el);else{if(f=e.el=s(e.type,c,g&&g.is,g),8&v?a(f,e.children):16&v&&T(e.children,f,null,i,l,c&&"foreignObject"!==m,u,p),b&&jo(e,null,i,"created"),g){for(const t in g)"value"===t||U(t)||r(f,t,null,g[t],c,e.children,i,l,J);"value"in g&&r(f,"value",null,g.value),(h=g.onVnodeBeforeMount)&&bs(h,i,e)}k(f,e,e.scopeId,u,i)}b&&jo(e,null,i,"beforeMount");const S=(!l||l&&!l.pendingBranch)&&y&&!y.persisted;S&&y.beforeEnter(f),n(f,t,o),((h=g&&g.onVnodeMounted)||S||b)&&Pr((()=>{h&&bs(h,i,e),S&&y.enter(f),b&&jo(e,null,i,"mounted")}),l)},k=(e,t,n,o,r)=>{if(n&&f(e,n),o)for(let s=0;s<o.length;s++)f(e,o[s]);if(r){if(t===r.subTree){const t=r.vnode;k(e,t,t.scopeId,t.slotScopeIds,r.parent)}}},T=(e,t,n,o,r,s,i,l,c=0)=>{for(let a=c;a<e.length;a++){const c=e[a]=l?vs(e[a]):gs(e[a]);m(null,c,t,n,o,r,s,i,l)}},E=(e,t,n,o,s,i,l)=>{const c=t.el=e.el;let{patchFlag:u,dynamicChildren:p,dirs:f}=t;u|=16&e.patchFlag;const d=e.props||v,h=t.props||v;let m;n&&Ir(n,!1),(m=h.onVnodeBeforeUpdate)&&bs(m,n,t,e),f&&jo(t,e,n,"beforeUpdate"),n&&Ir(n,!0);const g=s&&"foreignObject"!==t.type;if(p?$(e.dynamicChildren,p,c,n,o,g,i):l||B(e,t,c,null,n,o,g,i,!1),u>0){if(16&u)O(c,t,d,h,n,o,s);else if(2&u&&d.class!==h.class&&r(c,"class",null,h.class,s),4&u&&r(c,"style",d.style,h.style,s),8&u){const i=t.dynamicProps;for(let t=0;t<i.length;t++){const l=i[t],a=d[l],u=h[l];u===a&&"value"!==l||r(c,l,a,u,s,e.children,n,o,J)}}1&u&&e.children!==t.children&&a(c,t.children)}else l||null!=p||O(c,t,d,h,n,o,s);((m=h.onVnodeUpdated)||f)&&Pr((()=>{m&&bs(m,n,t,e),f&&jo(t,e,n,"updated")}),o)},$=(e,t,n,o,r,s,i)=>{for(let l=0;l<t.length;l++){const c=e[l],a=t[l],p=c.el&&(c.type===Wr||!rs(c,a)||70&c.shapeFlag)?u(c.el):n;m(c,a,p,null,o,r,s,i,!0)}},O=(e,t,n,o,s,i,l)=>{if(n!==o){for(const c in o){if(U(c))continue;const a=o[c],u=n[c];a!==u&&"value"!==c&&r(e,c,u,a,l,t.children,s,i,J)}if(n!==v)for(const c in n)U(c)||c in o||r(e,c,n[c],null,l,t.children,s,i,J);"value"in o&&r(e,"value",n.value,o.value)}},R=(e,t,o,r,s,l,c,a,u)=>{const p=t.el=e?e.el:i(""),f=t.anchor=e?e.anchor:i("");let{patchFlag:d,dynamicChildren:h,slotScopeIds:m}=t;m&&(a=a?a.concat(m):m),null==e?(n(p,o,r),n(f,o,r),T(t.children,o,f,s,l,c,a,u)):d>0&&64&d&&h&&e.dynamicChildren?($(e.dynamicChildren,h,o,s,l,c,a),(null!=t.key||s&&t===s.subTree)&&Br(e,t,!0)):B(e,t,o,f,s,l,c,a,u)},F=(e,t,n,o,r,s,i,l,c)=>{t.slotScopeIds=l,null==e?512&t.shapeFlag?r.ctx.activate(t,n,o,i,c):P(t,n,o,r,s,i,c):A(e,t,c)},P=(e,t,n,o,r,s,i)=>{const l=e.component=function(e,t,n){const o=e.type,r=(t?t.appContext:e.appContext)||Ss,s={uid:xs++,vnode:e,type:o,parent:t,appContext:r,root:null,next:null,subTree:null,effect:null,update:null,scope:new ne(!0),render:null,proxy:null,exposed:null,exposeProxy:null,withProxy:null,provides:t?t.provides:Object.create(r.provides),accessCache:null,renderCache:[],components:null,directives:null,propsOptions:mr(o,r),emitsOptions:Tn(o,r),emit:null,emitted:null,propsDefaults:v,inheritAttrs:o.inheritAttrs,ctx:v,data:v,props:v,attrs:v,slots:v,refs:v,setupState:v,setupContext:null,suspense:n,suspenseId:n?n.pendingId:0,asyncDep:null,asyncResolved:!1,isMounted:!1,isUnmounted:!1,isDeactivated:!1,bc:null,c:null,bm:null,m:null,bu:null,u:null,um:null,bum:null,da:null,a:null,rtg:null,rtc:null,ec:null,sp:null};s.ctx={_:s},s.root=t?t.root:s,s.emit=kn.bind(null,s),e.ce&&e.ce(s);return s}(e,o,r);if(yo(e)&&(l.ctx.renderer=te),function(e,t=!1){Os=t;const{props:n,children:o}=e.vnode,r=Ns(e);(function(e,t,n,o=!1){const r={},s={};Q(s,is,1),e.propsDefaults=Object.create(null),dr(e,t,r,s);for(const i in e.propsOptions[0])i in r||(r[i]=void 0);e.props=n?o?r:vt(r):e.type.props?r:s,e.attrs=s})(e,n,r,t),((e,t)=>{if(32&e.vnode.shapeFlag){const n=t._;n?(e.slots=kt(t),Q(t,"_",n)):Cr(t,e.slots={})}else e.slots={},t&&wr(e,t);Q(e.slots,is,1)})(e,o);const s=r?function(e,t){const n=e.type;e.accessCache=Object.create(null),e.proxy=Tt(new Proxy(e.ctx,er));const{setup:o}=n;if(o){const n=e.setupContext=o.length>1?Ms(e):null;ks(e),xe();const r=Yt(o,e,0,[e.props,n]);if(Ce(),Ts(),V(r)){if(r.then(Ts,Ts),t)return r.then((n=>{Rs(e,n,t)})).catch((t=>{Qt(t,e,0)}));e.asyncDep=r}else Rs(e,r,t)}else As(e,t)}(e,t):void 0;Os=!1}(l),l.asyncDep){if(r&&r.registerDep(l,M),!e.el){const e=l.subTree=us(Kr);b(null,e,t,n)}}else M(l,e,t,n,r,s,i)},A=(e,t,n)=>{const o=t.component=e.component;if(function(e,t,n){const{props:o,children:r,component:s}=e,{props:i,children:l,patchFlag:c}=t,a=s.emitsOptions;if(t.dirs||t.transition)return!0;if(!(n&&c>=0))return!(!r&&!l||l&&l.$stable)||o!==i&&(o?!i||Bn(o,i,a):!!i);if(1024&c)return!0;if(16&c)return o?Bn(o,i,a):!!i;if(8&c){const e=t.dynamicProps;for(let t=0;t<e.length;t++){const n=e[t];if(i[n]!==o[n]&&!Nn(a,n))return!0}}return!1}(e,t,n)){if(o.asyncDep&&!o.asyncResolved)return void I(o,t,n);o.next=t,function(e){const t=tn.indexOf(e);t>nn&&tn.splice(t,1)}(o.update),o.update()}else t.el=e.el,o.vnode=t},M=(e,t,n,o,r,s,i)=>{const l=e.effect=new ge((()=>{if(e.isMounted){let t,{next:n,bu:o,u:l,parent:c,vnode:a}=e,p=n;Ir(e,!1),n?(n.el=a.el,I(e,n,i)):n=a,o&&Z(o),(t=n.props&&n.props.onVnodeBeforeUpdate)&&bs(t,c,n,a),Ir(e,!0);const f=Mn(e),d=e.subTree;e.subTree=f,m(d,f,u(d.el),Y(d),e,r,s),n.el=f.el,null===p&&Ln(e,f.el),l&&Pr(l,r),(t=n.props&&n.props.onVnodeUpdated)&&Pr((()=>bs(t,c,n,a)),r)}else{let i;const{el:l,props:c}=t,{bm:a,m:u,parent:p}=e,f=mo(t);if(Ir(e,!1),a&&Z(a),!f&&(i=c&&c.onVnodeBeforeMount)&&bs(i,p,t),Ir(e,!0),l&&re){const n=()=>{e.subTree=Mn(e),re(l,e.subTree,e,r,null)};f?t.type.__asyncLoader().then((()=>!e.isUnmounted&&n())):n()}else{const i=e.subTree=Mn(e);m(null,i,n,o,e,r,s),t.el=i.el}if(u&&Pr(u,r),!f&&(i=c&&c.onVnodeMounted)){const e=t;Pr((()=>bs(i,p,e)),r)}(256&t.shapeFlag||p&&mo(p.vnode)&&256&p.vnode.shapeFlag)&&e.a&&Pr(e.a,r),e.isMounted=!0,t=n=o=null}}),(()=>hn(c)),e.scope),c=e.update=()=>l.run();c.id=e.uid,Ir(e,!0),c()},I=(e,t,n)=>{t.component=e;const o=e.vnode.props;e.vnode=t,e.next=null,function(e,t,n,o){const{props:r,attrs:s,vnode:{patchFlag:i}}=e,l=kt(r),[c]=e.propsOptions;let a=!1;if(!(o||i>0)||16&i){let o;dr(e,t,r,s)&&(a=!0);for(const s in l)t&&(N(t,s)||(o=G(s))!==s&&N(t,o))||(c?!n||void 0===n[s]&&void 0===n[o]||(r[s]=hr(c,l,s,void 0,e,!0)):delete r[s]);if(s!==l)for(const e in s)t&&N(t,e)||(delete s[e],a=!0)}else if(8&i){const n=e.vnode.dynamicProps;for(let o=0;o<n.length;o++){let i=n[o];if(Nn(e.emitsOptions,i))continue;const u=t[i];if(c)if(N(s,i))u!==s[i]&&(s[i]=u,a=!0);else{const t=z(i);r[t]=hr(c,l,t,u,e,!1)}else u!==s[i]&&(s[i]=u,a=!0)}}a&&Te(e,"set","$attrs")}(e,t.props,o,n),((e,t,n)=>{const{vnode:o,slots:r}=e;let s=!0,i=v;if(32&o.shapeFlag){const e=t._;e?n&&1===e?s=!1:(w(r,t),n||1!==e||delete r._):(s=!t.$stable,Cr(t,r)),i=t}else t&&(wr(e,t),i={default:1});if(s)for(const l in r)br(l)||l in i||delete r[l]})(e,t.children,n),xe(),yn(void 0,e.update),Ce()},B=(e,t,n,o,r,s,i,l,c=!1)=>{const u=e&&e.children,p=e?e.shapeFlag:0,f=t.children,{patchFlag:d,shapeFlag:h}=t;if(d>0){if(128&d)return void j(u,f,n,o,r,s,i,l,c);if(256&d)return void L(u,f,n,o,r,s,i,l,c)}8&h?(16&p&&J(u,r,s),f!==u&&a(n,f)):16&p?16&h?j(u,f,n,o,r,s,i,l,c):J(u,r,s,!0):(8&p&&a(n,""),16&h&&T(f,n,o,r,s,i,l,c))},L=(e,t,n,o,r,s,i,l,c)=>{const a=(e=e||y).length,u=(t=t||y).length,p=Math.min(a,u);let f;for(f=0;f<p;f++){const o=t[f]=c?vs(t[f]):gs(t[f]);m(e[f],o,n,null,r,s,i,l,c)}a>u?J(e,r,s,!0,!1,p):T(t,n,o,r,s,i,l,c,p)},j=(e,t,n,o,r,s,i,l,c)=>{let a=0;const u=t.length;let p=e.length-1,f=u-1;for(;a<=p&&a<=f;){const o=e[a],u=t[a]=c?vs(t[a]):gs(t[a]);if(!rs(o,u))break;m(o,u,n,null,r,s,i,l,c),a++}for(;a<=p&&a<=f;){const o=e[p],a=t[f]=c?vs(t[f]):gs(t[f]);if(!rs(o,a))break;m(o,a,n,null,r,s,i,l,c),p--,f--}if(a>p){if(a<=f){const e=f+1,p=e<u?t[e].el:o;for(;a<=f;)m(null,t[a]=c?vs(t[a]):gs(t[a]),n,p,r,s,i,l,c),a++}}else if(a>f)for(;a<=p;)H(e[a],r,s,!0),a++;else{const d=a,h=a,g=new Map;for(a=h;a<=f;a++){const e=t[a]=c?vs(t[a]):gs(t[a]);null!=e.key&&g.set(e.key,a)}let v,_=0;const b=f-h+1;let S=!1,x=0;const C=new Array(b);for(a=0;a<b;a++)C[a]=0;for(a=d;a<=p;a++){const o=e[a];if(_>=b){H(o,r,s,!0);continue}let u;if(null!=o.key)u=g.get(o.key);else for(v=h;v<=f;v++)if(0===C[v-h]&&rs(o,t[v])){u=v;break}void 0===u?H(o,r,s,!0):(C[u-h]=a+1,u>=x?x=u:S=!0,m(o,t[u],n,null,r,s,i,l,c),_++)}const w=S?function(e){const t=e.slice(),n=[0];let o,r,s,i,l;const c=e.length;for(o=0;o<c;o++){const c=e[o];if(0!==c){if(r=n[n.length-1],e[r]<c){t[o]=r,n.push(o);continue}for(s=0,i=n.length-1;s<i;)l=s+i>>1,e[n[l]]<c?s=l+1:i=l;c<e[n[s]]&&(s>0&&(t[o]=n[s-1]),n[s]=o)}}s=n.length,i=n[s-1];for(;s-- >0;)n[s]=i,i=t[i];return n}(C):y;for(v=w.length-1,a=b-1;a>=0;a--){const e=h+a,p=t[e],f=e+1<u?t[e+1].el:o;0===C[a]?m(null,p,n,f,r,s,i,l,c):S&&(v<0||a!==w[v]?D(p,n,f,2):v--)}}},D=(e,t,o,r,s=null)=>{const{el:i,type:l,transition:c,children:a,shapeFlag:u}=e;if(6&u)return void D(e.component.subTree,t,o,r);if(128&u)return void e.suspense.move(t,o,r);if(64&u)return void l.move(e,t,o,te);if(l===Wr){n(i,t,o);for(let e=0;e<a.length;e++)D(a[e],t,o,r);return void n(e.anchor,t,o)}if(l===Gr)return void(({el:e,anchor:t},o,r)=>{let s;for(;e&&e!==t;)s=p(e),n(e,o,r),e=s;n(t,o,r)})(e,t,o);if(2!==r&&1&u&&c)if(0===r)c.beforeEnter(i),n(i,t,o),Pr((()=>c.enter(i)),s);else{const{leave:e,delayLeave:r,afterLeave:s}=c,l=()=>n(i,t,o),a=()=>{e(i,(()=>{l(),s&&s()}))};r?r(i,l,a):a()}else n(i,t,o)},H=(e,t,n,o=!1,r=!1)=>{const{type:s,props:i,ref:l,children:c,dynamicChildren:a,shapeFlag:u,patchFlag:p,dirs:f}=e;if(null!=l&&Er(l,null,n,e,!0),256&u)return void t.ctx.deactivate(e);const d=1&u&&f,h=!mo(e);let m;if(h&&(m=i&&i.onVnodeBeforeUnmount)&&bs(m,t,e),6&u)q(e.component,n,o);else{if(128&u)return void e.suspense.unmount(n,o);d&&jo(e,null,t,"beforeUnmount"),64&u?e.type.remove(e,t,n,r,te,o):a&&(s!==Wr||p>0&&64&p)?J(a,t,n,!1,!0):(s===Wr&&384&p||!r&&16&u)&&J(c,t,n),o&&W(e)}(h&&(m=i&&i.onVnodeUnmounted)||d)&&Pr((()=>{m&&bs(m,t,e),d&&jo(e,null,t,"unmounted")}),n)},W=e=>{const{type:t,el:n,anchor:r,transition:s}=e;if(t===Wr)return void K(n,r);if(t===Gr)return void(({el:e,anchor:t})=>{let n;for(;e&&e!==t;)n=p(e),o(e),e=n;o(t)})(e);const i=()=>{o(n),s&&!s.persisted&&s.afterLeave&&s.afterLeave()};if(1&e.shapeFlag&&s&&!s.persisted){const{leave:t,delayLeave:o}=s,r=()=>t(n,i);o?o(e.el,i,r):r()}else i()},K=(e,t)=>{let n;for(;e!==t;)n=p(e),o(e),e=n;o(t)},q=(e,t,n)=>{const{bum:o,scope:r,update:s,subTree:i,um:l}=e;o&&Z(o),r.stop(),s&&(s.active=!1,H(i,e,t,n)),l&&Pr(l,t),Pr((()=>{e.isUnmounted=!0}),t),t&&t.pendingBranch&&!t.isUnmounted&&e.asyncDep&&!e.asyncResolved&&e.suspenseId===t.pendingId&&(t.deps--,0===t.deps&&t.resolve())},J=(e,t,n,o=!1,r=!1,s=0)=>{for(let i=s;i<e.length;i++)H(e[i],t,n,o,r)},Y=e=>6&e.shapeFlag?Y(e.component.subTree):128&e.shapeFlag?e.suspense.next():p(e.anchor||e.el),X=(e,t,n)=>{null==e?t._vnode&&H(t._vnode,null,null,!0):m(t._vnode||null,e,t,null,null,null,n),_n(),t._vnode=e},te={p:m,um:H,m:D,r:W,mt:P,mc:T,pc:B,pbc:$,n:Y,o:e};let oe,re;return t&&([oe,re]=t(te)),{render:X,hydrate:oe,createApp:Nr(X,oe)}}function Ir({effect:e,update:t},n){e.allowRecurse=t.allowRecurse=n}function Br(e,t,n=!1){const o=e.children,r=t.children;if(E(o)&&E(r))for(let s=0;s<o.length;s++){const e=o[s];let t=r[s];1&t.shapeFlag&&!t.dynamicChildren&&((t.patchFlag<=0||32===t.patchFlag)&&(t=r[s]=vs(r[s]),t.el=e.el),n||Br(e,t))}}const Lr=e=>e&&(e.disabled||""===e.disabled),jr=e=>"undefined"!=typeof SVGElement&&e instanceof SVGElement,Ur=(e,t)=>{const n=e&&e.to;if(P(n)){if(t){return t(n)}return null}return n};function Dr(e,t,n,{o:{insert:o},m:r},s=2){0===s&&o(e.targetAnchor,t,n);const{el:i,anchor:l,shapeFlag:c,children:a,props:u}=e,p=2===s;if(p&&o(i,t,n),(!p||Lr(u))&&16&c)for(let f=0;f<a.length;f++)r(a[f],t,n,2);p&&o(l,t,n)}const Hr={__isTeleport:!0,process(e,t,n,o,r,s,i,l,c,a){const{mc:u,pc:p,pbc:f,o:{insert:d,querySelector:h,createText:m}}=a,g=Lr(t.props);let{shapeFlag:v,children:y,dynamicChildren:_}=t;if(null==e){const e=t.el=m(""),a=t.anchor=m("");d(e,n,o),d(a,n,o);const p=t.target=Ur(t.props,h),f=t.targetAnchor=m("");p&&(d(f,p),i=i||jr(p));const _=(e,t)=>{16&v&&u(y,e,t,r,s,i,l,c)};g?_(n,a):p&&_(p,f)}else{t.el=e.el;const o=t.anchor=e.anchor,u=t.target=e.target,d=t.targetAnchor=e.targetAnchor,m=Lr(e.props),v=m?n:u,y=m?o:d;if(i=i||jr(u),_?(f(e.dynamicChildren,_,v,r,s,i,l),Br(e,t,!0)):c||p(e,t,v,y,r,s,i,l,!1),g)m||Dr(t,n,o,a,1);else if((t.props&&t.props.to)!==(e.props&&e.props.to)){const e=t.target=Ur(t.props,h);e&&Dr(t,e,null,a,0)}else m&&Dr(t,u,d,a,1)}},remove(e,t,n,o,{um:r,o:{remove:s}},i){const{shapeFlag:l,children:c,anchor:a,targetAnchor:u,target:p,props:f}=e;if(p&&s(u),(i||!Lr(f))&&(s(a),16&l))for(let d=0;d<c.length;d++){const e=c[d];r(e,t,n,!0,!!e.dynamicChildren)}},move:Dr,hydrate:function(e,t,n,o,r,s,{o:{nextSibling:i,parentNode:l,querySelector:c}},a){const u=t.target=Ur(t.props,c);if(u){const c=u._lpa||u.firstChild;if(16&t.shapeFlag)if(Lr(t.props))t.anchor=a(i(e),t,l(e),n,o,r,s),t.targetAnchor=c;else{t.anchor=i(e);let l=c;for(;l;)if(l=i(l),l&&8===l.nodeType&&"teleport anchor"===l.data){t.targetAnchor=l,u._lpa=t.targetAnchor&&i(t.targetAnchor);break}a(c,t,u,n,o,r,s)}}return t.anchor&&i(t.anchor)}},Wr=Symbol(void 0),zr=Symbol(void 0),Kr=Symbol(void 0),Gr=Symbol(void 0),qr=[];let Jr=null;function Yr(e=!1){qr.push(Jr=e?null:[])}function Zr(){qr.pop(),Jr=qr[qr.length-1]||null}let Qr=1;function Xr(e){Qr+=e}function es(e){return e.dynamicChildren=Qr>0?Jr||y:null,Zr(),Qr>0&&Jr&&Jr.push(e),e}function ts(e,t,n,o,r,s){return es(as(e,t,n,o,r,s,!0))}function ns(e,t,n,o,r){return es(us(e,t,n,o,r,!0))}function os(e){return!!e&&!0===e.__v_isVNode}function rs(e,t){return e.type===t.type&&e.key===t.key}function ss(e){}const is="__vInternal",ls=({key:e})=>null!=e?e:null,cs=({ref:e,ref_key:t,ref_for:n})=>null!=e?P(e)||Rt(e)||F(e)?{i:En,r:e,k:t,f:!!n}:e:null;function as(e,t=null,n=null,o=0,r=null,s=(e===Wr?0:1),i=!1,l=!1){const c={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&ls(t),ref:t&&cs(t),scopeId:$n,slotScopeIds:null,children:n,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetAnchor:null,staticCount:0,shapeFlag:s,patchFlag:o,dynamicProps:r,dynamicChildren:null,appContext:null};return l?(ys(c,n),128&s&&e.normalize(c)):n&&(c.shapeFlag|=P(n)?8:16),Qr>0&&!i&&Jr&&(c.patchFlag>0||6&s)&&32!==c.patchFlag&&Jr.push(c),c}const us=function(e,t=null,n=null,o=0,s=null,i=!1){e&&e!==Do||(e=Kr);if(os(e)){const o=fs(e,t,!0);return n&&ys(o,n),Qr>0&&!i&&Jr&&(6&o.shapeFlag?Jr[Jr.indexOf(e)]=o:Jr.push(o)),o.patchFlag|=-2,o}l=e,F(l)&&"__vccOpts"in l&&(e=e.__vccOpts);var l;if(t){t=ps(t);let{class:e,style:n}=t;e&&!P(e)&&(t.class=c(e)),M(n)&&(wt(n)&&!E(n)&&(n=w({},n)),t.style=r(n))}const a=P(e)?1:jn(e)?128:(e=>e.__isTeleport)(e)?64:M(e)?4:F(e)?2:0;return as(e,t,n,o,s,a,i,!0)};function ps(e){return e?wt(e)||is in e?w({},e):e:null}function fs(e,t,n=!1){const{props:o,ref:r,patchFlag:s,children:i}=e,l=t?_s(o||{},t):o;return{__v_isVNode:!0,__v_skip:!0,type:e.type,props:l,key:l&&ls(l),ref:t&&t.ref?n&&r?E(r)?r.concat(cs(t)):[r,cs(t)]:cs(t):r,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:i,target:e.target,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==Wr?-1===s?16:16|s:s,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:e.transition,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&fs(e.ssContent),ssFallback:e.ssFallback&&fs(e.ssFallback),el:e.el,anchor:e.anchor}}function ds(e=" ",t=0){return us(zr,null,e,t)}function hs(e,t){const n=us(Gr,null,e);return n.staticCount=t,n}function ms(e="",t=!1){return t?(Yr(),ns(Kr,null,e)):us(Kr,null,e)}function gs(e){return null==e||"boolean"==typeof e?us(Kr):E(e)?us(Wr,null,e.slice()):"object"==typeof e?vs(e):us(zr,null,String(e))}function vs(e){return null===e.el||e.memo?e:fs(e)}function ys(e,t){let n=0;const{shapeFlag:o}=e;if(null==t)t=null;else if(E(t))n=16;else if("object"==typeof t){if(65&o){const n=t.default;return void(n&&(n._c&&(n._d=!1),ys(e,n()),n._c&&(n._d=!0)))}{n=32;const o=t._;o||is in t?3===o&&En&&(1===En.slots._?t._=1:(t._=2,e.patchFlag|=1024)):t._ctx=En}}else F(t)?(t={default:t,_ctx:En},n=32):(t=String(t),64&o?(n=16,t=[ds(t)]):n=8);e.children=t,e.shapeFlag|=n}function _s(...e){const t={};for(let n=0;n<e.length;n++){const o=e[n];for(const e in o)if("class"===e)t.class!==o.class&&(t.class=c([t.class,o.class]));else if("style"===e)t.style=r([t.style,o.style]);else if(x(e)){const n=t[e],r=o[e];!r||n===r||E(n)&&n.includes(r)||(t[e]=n?[].concat(n,r):r)}else""!==e&&(t[e]=o[e])}return t}function bs(e,t,n,o=null){Zt(e,t,7,[n,o])}const Ss=kr();let xs=0;let Cs=null;const ws=()=>Cs||En,ks=e=>{Cs=e,e.scope.on()},Ts=()=>{Cs&&Cs.scope.off(),Cs=null};function Ns(e){return 4&e.vnode.shapeFlag}let Es,$s,Os=!1;function Rs(e,t,n){F(t)?e.render=t:M(t)&&(e.setupState=Lt(t)),As(e,n)}function Fs(e){Es=e,$s=e=>{e.render._rc&&(e.withProxy=new Proxy(e.ctx,tr))}}const Ps=()=>!Es;function As(e,t,n){const o=e.type;if(!e.render){if(!t&&Es&&!o.render){const t=o.template;if(t){const{isCustomElement:n,compilerOptions:r}=e.appContext.config,{delimiters:s,compilerOptions:i}=o,l=w(w({isCustomElement:n,delimiters:s},r),i);o.render=Es(t,l)}}e.render=o.render||_,$s&&$s(e)}ks(e),xe(),or(e),Ce(),Ts()}function Ms(e){const t=t=>{e.exposed=t||{}};let n;return{get attrs(){return n||(n=function(e){return new Proxy(e.attrs,{get:(t,n)=>(we(e,0,"$attrs"),t[n])})}(e))},slots:e.slots,emit:e.emit,expose:t}}function Vs(e){if(e.exposed)return e.exposeProxy||(e.exposeProxy=new Proxy(Lt(Tt(e.exposed)),{get:(t,n)=>n in t?t[n]:n in Xo?Xo[n](e):void 0}))}const Is=/(?:^|[-_])(\w)/g;function Bs(e,t=!0){return F(e)?e.displayName||e.name:e.name||t&&e.__name}function Ls(e,t,n=!1){let o=Bs(t);if(!o&&t.__file){const e=t.__file.match(/([^/\\]+)\.\w+$/);e&&(o=e[1])}if(!o&&e&&e.parent){const n=e=>{for(const n in e)if(e[n]===t)return n};o=n(e.components||e.parent.type.components)||n(e.appContext.components)}return o?o.replace(Is,(e=>e.toUpperCase())).replace(/[-_]/g,""):n?"App":"Anonymous"}const js=(e,t)=>function(e,t,n=!1){let o,r;const s=F(e);return s?(o=e,r=_):(o=e.get,r=e.set),new zt(o,r,s||!r,n)}(e,0,Os);function Us(){return null}function Ds(){return null}function Hs(e){}function Ws(e,t){return null}function zs(){return Gs().slots}function Ks(){return Gs().attrs}function Gs(){const e=ws();return e.setupContext||(e.setupContext=Ms(e))}function qs(e,t){const n=E(e)?e.reduce(((e,t)=>(e[t]={},e)),{}):e;for(const o in t){const e=n[o];e?E(e)||F(e)?n[o]={type:e,default:t[o]}:e.default=t[o]:null===e&&(n[o]={default:t[o]})}return n}function Js(e,t){const n={};for(const o in e)t.includes(o)||Object.defineProperty(n,o,{enumerable:!0,get:()=>e[o]});return n}function Ys(e){const t=ws();let n=e();return Ts(),V(n)&&(n=n.catch((e=>{throw ks(t),e}))),[n,()=>ks(t)]}function Zs(e,t,n){const o=arguments.length;return 2===o?M(t)&&!E(t)?os(t)?us(e,null,[t]):us(e,t):us(e,null,t):(o>3?n=Array.prototype.slice.call(arguments,2):3===o&&os(n)&&(n=[n]),us(e,t,n))}const Qs=Symbol(""),Xs=()=>{{const e=qn(Qs);return e||Gt("Server rendering context not provided. Make sure to only call useSSRContext() conditionally in the server build."),e}};function ei(){}function ti(e,t,n,o){const r=n[o];if(r&&ni(r,e))return r;const s=t();return s.memo=e.slice(),n[o]=s}function ni(e,t){const n=e.memo;if(n.length!=t.length)return!1;for(let o=0;o<n.length;o++)if(Y(n[o],t[o]))return!1;return Qr>0&&Jr&&Jr.push(e),!0}const oi="3.2.37",ri=null,si=null,ii=null,li="undefined"!=typeof document?document:null,ci=li&&li.createElement("template"),ai={insert:(e,t,n)=>{t.insertBefore(e,n||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,n,o)=>{const r=t?li.createElementNS("http://www.w3.org/2000/svg",e):li.createElement(e,n?{is:n}:void 0);return"select"===e&&o&&null!=o.multiple&&r.setAttribute("multiple",o.multiple),r},createText:e=>li.createTextNode(e),createComment:e=>li.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>li.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},cloneNode(e){const t=e.cloneNode(!0);return"_value"in e&&(t._value=e._value),t},insertStaticContent(e,t,n,o,r,s){const i=n?n.previousSibling:t.lastChild;if(r&&(r===s||r.nextSibling))for(;t.insertBefore(r.cloneNode(!0),n),r!==s&&(r=r.nextSibling););else{ci.innerHTML=o?`<svg>${e}</svg>`:e;const r=ci.content;if(o){const e=r.firstChild;for(;e.firstChild;)r.appendChild(e.firstChild);r.removeChild(e)}t.insertBefore(r,n)}return[i?i.nextSibling:t.firstChild,n?n.previousSibling:t.lastChild]}};const ui=/\s*!important$/;function pi(e,t,n){if(E(n))n.forEach((n=>pi(e,t,n)));else if(null==n&&(n=""),t.startsWith("--"))e.setProperty(t,n);else{const o=function(e,t){const n=di[t];if(n)return n;let o=z(t);if("filter"!==o&&o in e)return di[t]=o;o=q(o);for(let r=0;r<fi.length;r++){const n=fi[r]+o;if(n in e)return di[t]=n}return t}(e,t);ui.test(n)?e.setProperty(G(o),n.replace(ui,""),"important"):e[o]=n}}const fi=["Webkit","Moz","ms"],di={};const hi="http://www.w3.org/1999/xlink";const[mi,gi]=(()=>{let e=Date.now,t=!1;if("undefined"!=typeof window){Date.now()>document.createEvent("Event").timeStamp&&(e=performance.now.bind(performance));const n=navigator.userAgent.match(/firefox\/(\d+)/i);t=!!(n&&Number(n[1])<=53)}return[e,t]})();let vi=0;const yi=Promise.resolve(),_i=()=>{vi=0};function bi(e,t,n,o){e.addEventListener(t,n,o)}function Si(e,t,n,o,r=null){const s=e._vei||(e._vei={}),i=s[t];if(o&&i)i.value=o;else{const[n,l]=function(e){let t;if(xi.test(e)){let n;for(t={};n=e.match(xi);)e=e.slice(0,e.length-n[0].length),t[n[0].toLowerCase()]=!0}return[G(e.slice(2)),t]}(t);if(o){const i=s[t]=function(e,t){const n=e=>{const o=e.timeStamp||mi();(gi||o>=n.attached-1)&&Zt(function(e,t){if(E(t)){const n=e.stopImmediatePropagation;return e.stopImmediatePropagation=()=>{n.call(e),e._stopped=!0},t.map((e=>t=>!t._stopped&&e&&e(t)))}return t}(e,n.value),t,5,[e])};return n.value=e,n.attached=(()=>vi||(yi.then(_i),vi=mi()))(),n}(o,r);bi(e,n,i,l)}else i&&(!function(e,t,n,o){e.removeEventListener(t,n,o)}(e,n,i,l),s[t]=void 0)}}const xi=/(?:Once|Passive|Capture)$/;const Ci=/^on[a-z]/;function wi(e,t){const n=ho(e);class o extends Ni{constructor(e){super(n,e,t)}}return o.def=n,o}const ki=e=>wi(e,Tl),Ti="undefined"!=typeof HTMLElement?HTMLElement:class{};class Ni extends Ti{constructor(e,t={},n){super(),this._def=e,this._props=t,this._instance=null,this._connected=!1,this._resolved=!1,this._numberProps=null,this.shadowRoot&&n?n(this._createVNode(),this.shadowRoot):this.attachShadow({mode:"open"})}connectedCallback(){this._connected=!0,this._instance||this._resolveDef()}disconnectedCallback(){this._connected=!1,dn((()=>{this._connected||(kl(null,this.shadowRoot),this._instance=null)}))}_resolveDef(){if(this._resolved)return;this._resolved=!0;for(let n=0;n<this.attributes.length;n++)this._setAttr(this.attributes[n].name);new MutationObserver((e=>{for(const t of e)this._setAttr(t.attributeName)})).observe(this,{attributes:!0});const e=e=>{const{props:t,styles:n}=e,o=!E(t),r=t?o?Object.keys(t):t:[];let s;if(o)for(const i in this._props){const e=t[i];(e===Number||e&&e.type===Number)&&(this._props[i]=X(this._props[i]),(s||(s=Object.create(null)))[i]=!0)}this._numberProps=s;for(const i of Object.keys(this))"_"!==i[0]&&this._setProp(i,this[i],!0,!1);for(const i of r.map(z))Object.defineProperty(this,i,{get(){return this._getProp(i)},set(e){this._setProp(i,e)}});this._applyStyles(n),this._update()},t=this._def.__asyncLoader;t?t().then(e):e(this._def)}_setAttr(e){let t=this.getAttribute(e);this._numberProps&&this._numberProps[e]&&(t=X(t)),this._setProp(z(e),t,!1)}_getProp(e){return this._props[e]}_setProp(e,t,n=!0,o=!0){t!==this._props[e]&&(this._props[e]=t,o&&this._instance&&this._update(),n&&(!0===t?this.setAttribute(G(e),""):"string"==typeof t||"number"==typeof t?this.setAttribute(G(e),t+""):t||this.removeAttribute(G(e))))}_update(){kl(this._createVNode(),this.shadowRoot)}_createVNode(){const e=us(this._def,w({},this._props));return this._instance||(e.ce=e=>{this._instance=e,e.isCE=!0,e.emit=(e,...t)=>{this.dispatchEvent(new CustomEvent(e,{detail:t}))};let t=this;for(;t=t&&(t.parentNode||t.host);)if(t instanceof Ni){e.parent=t._instance;break}}),e}_applyStyles(e){e&&e.forEach((e=>{const t=document.createElement("style");t.textContent=e,this.shadowRoot.appendChild(t)}))}}function Ei(e="$style"){{const t=ws();if(!t)return v;const n=t.type.__cssModules;if(!n)return v;const o=n[e];return o||v}}function $i(e){const t=ws();if(!t)return;const n=()=>Oi(t.subTree,e(t.proxy));Yn(n),Oo((()=>{const e=new MutationObserver(n);e.observe(t.subTree.el.parentNode,{childList:!0}),Ao((()=>e.disconnect()))}))}function Oi(e,t){if(128&e.shapeFlag){const n=e.suspense;e=n.activeBranch,n.pendingBranch&&!n.isHydrating&&n.effects.push((()=>{Oi(n.activeBranch,t)}))}for(;e.component;)e=e.component.subTree;if(1&e.shapeFlag&&e.el)Ri(e.el,t);else if(e.type===Wr)e.children.forEach((e=>Oi(e,t)));else if(e.type===Gr){let{el:n,anchor:o}=e;for(;n&&(Ri(n,t),n!==o);)n=n.nextSibling}}function Ri(e,t){if(1===e.nodeType){const n=e.style;for(const e in t)n.setProperty(`--${e}`,t[e])}}const Fi=(e,{slots:t})=>Zs(io,Ii(e),t);Fi.displayName="Transition";const Pi={name:String,type:String,css:{type:Boolean,default:!0},duration:[String,Number,Object],enterFromClass:String,enterActiveClass:String,enterToClass:String,appearFromClass:String,appearActiveClass:String,appearToClass:String,leaveFromClass:String,leaveActiveClass:String,leaveToClass:String},Ai=Fi.props=w({},io.props,Pi),Mi=(e,t=[])=>{E(e)?e.forEach((e=>e(...t))):e&&e(...t)},Vi=e=>!!e&&(E(e)?e.some((e=>e.length>1)):e.length>1);function Ii(e){const t={};for(const w in e)w in Pi||(t[w]=e[w]);if(!1===e.css)return t;const{name:n="v",type:o,duration:r,enterFromClass:s=`${n}-enter-from`,enterActiveClass:i=`${n}-enter-active`,enterToClass:l=`${n}-enter-to`,appearFromClass:c=s,appearActiveClass:a=i,appearToClass:u=l,leaveFromClass:p=`${n}-leave-from`,leaveActiveClass:f=`${n}-leave-active`,leaveToClass:d=`${n}-leave-to`}=e,h=function(e){if(null==e)return null;if(M(e))return[Bi(e.enter),Bi(e.leave)];{const t=Bi(e);return[t,t]}}(r),m=h&&h[0],g=h&&h[1],{onBeforeEnter:v,onEnter:y,onEnterCancelled:_,onLeave:b,onLeaveCancelled:S,onBeforeAppear:x=v,onAppear:C=y,onAppearCancelled:k=_}=t,T=(e,t,n)=>{ji(e,t?u:l),ji(e,t?a:i),n&&n()},N=(e,t)=>{e._isLeaving=!1,ji(e,p),ji(e,d),ji(e,f),t&&t()},E=e=>(t,n)=>{const r=e?C:y,i=()=>T(t,e,n);Mi(r,[t,i]),Ui((()=>{ji(t,e?c:s),Li(t,e?u:l),Vi(r)||Hi(t,o,m,i)}))};return w(t,{onBeforeEnter(e){Mi(v,[e]),Li(e,s),Li(e,i)},onBeforeAppear(e){Mi(x,[e]),Li(e,c),Li(e,a)},onEnter:E(!1),onAppear:E(!0),onLeave(e,t){e._isLeaving=!0;const n=()=>N(e,t);Li(e,p),Gi(),Li(e,f),Ui((()=>{e._isLeaving&&(ji(e,p),Li(e,d),Vi(b)||Hi(e,o,g,n))})),Mi(b,[e,n])},onEnterCancelled(e){T(e,!1),Mi(_,[e])},onAppearCancelled(e){T(e,!0),Mi(k,[e])},onLeaveCancelled(e){N(e),Mi(S,[e])}})}function Bi(e){return X(e)}function Li(e,t){t.split(/\s+/).forEach((t=>t&&e.classList.add(t))),(e._vtc||(e._vtc=new Set)).add(t)}function ji(e,t){t.split(/\s+/).forEach((t=>t&&e.classList.remove(t)));const{_vtc:n}=e;n&&(n.delete(t),n.size||(e._vtc=void 0))}function Ui(e){requestAnimationFrame((()=>{requestAnimationFrame(e)}))}let Di=0;function Hi(e,t,n,o){const r=e._endId=++Di,s=()=>{r===e._endId&&o()};if(n)return setTimeout(s,n);const{type:i,timeout:l,propCount:c}=Wi(e,t);if(!i)return o();const a=i+"end";let u=0;const p=()=>{e.removeEventListener(a,f),s()},f=t=>{t.target===e&&++u>=c&&p()};setTimeout((()=>{u<c&&p()}),l+1),e.addEventListener(a,f)}function Wi(e,t){const n=window.getComputedStyle(e),o=e=>(n[e]||"").split(", "),r=o("transitionDelay"),s=o("transitionDuration"),i=zi(r,s),l=o("animationDelay"),c=o("animationDuration"),a=zi(l,c);let u=null,p=0,f=0;"transition"===t?i>0&&(u="transition",p=i,f=s.length):"animation"===t?a>0&&(u="animation",p=a,f=c.length):(p=Math.max(i,a),u=p>0?i>a?"transition":"animation":null,f=u?"transition"===u?s.length:c.length:0);return{type:u,timeout:p,propCount:f,hasTransform:"transition"===u&&/\b(transform|all)(,|$)/.test(n.transitionProperty)}}function zi(e,t){for(;e.length<t.length;)e=e.concat(e);return Math.max(...t.map(((t,n)=>Ki(t)+Ki(e[n]))))}function Ki(e){return 1e3*Number(e.slice(0,-1).replace(",","."))}function Gi(){return document.body.offsetHeight}const qi=new WeakMap,Ji=new WeakMap,Yi={name:"TransitionGroup",props:w({},Ai,{tag:String,moveClass:String}),setup(e,{slots:t}){const n=ws(),o=ro();let r,s;return Fo((()=>{if(!r.length)return;const t=e.moveClass||`${e.name||"v"}-move`;if(!function(e,t,n){const o=e.cloneNode();e._vtc&&e._vtc.forEach((e=>{e.split(/\s+/).forEach((e=>e&&o.classList.remove(e)))}));n.split(/\s+/).forEach((e=>e&&o.classList.add(e))),o.style.display="none";const r=1===t.nodeType?t:t.parentNode;r.appendChild(o);const{hasTransform:s}=Wi(o);return r.removeChild(o),s}(r[0].el,n.vnode.el,t))return;r.forEach(Zi),r.forEach(Qi);const o=r.filter(Xi);Gi(),o.forEach((e=>{const n=e.el,o=n.style;Li(n,t),o.transform=o.webkitTransform=o.transitionDuration="";const r=n._moveCb=e=>{e&&e.target!==n||e&&!/transform$/.test(e.propertyName)||(n.removeEventListener("transitionend",r),n._moveCb=null,ji(n,t))};n.addEventListener("transitionend",r)}))})),()=>{const i=kt(e),l=Ii(i);let c=i.tag||Wr;r=s,s=t.default?fo(t.default()):[];for(let e=0;e<s.length;e++){const t=s[e];null!=t.key&&po(t,co(t,l,o,n))}if(r)for(let e=0;e<r.length;e++){const t=r[e];po(t,co(t,l,o,n)),qi.set(t,t.el.getBoundingClientRect())}return us(c,null,s)}}};function Zi(e){const t=e.el;t._moveCb&&t._moveCb(),t._enterCb&&t._enterCb()}function Qi(e){Ji.set(e,e.el.getBoundingClientRect())}function Xi(e){const t=qi.get(e),n=Ji.get(e),o=t.left-n.left,r=t.top-n.top;if(o||r){const t=e.el.style;return t.transform=t.webkitTransform=`translate(${o}px,${r}px)`,t.transitionDuration="0s",e}}const el=e=>{const t=e.props["onUpdate:modelValue"]||!1;return E(t)?e=>Z(t,e):t};function tl(e){e.target.composing=!0}function nl(e){const t=e.target;t.composing&&(t.composing=!1,t.dispatchEvent(new Event("input")))}const ol={created(e,{modifiers:{lazy:t,trim:n,number:o}},r){e._assign=el(r);const s=o||r.props&&"number"===r.props.type;bi(e,t?"change":"input",(t=>{if(t.target.composing)return;let o=e.value;n&&(o=o.trim()),s&&(o=X(o)),e._assign(o)})),n&&bi(e,"change",(()=>{e.value=e.value.trim()})),t||(bi(e,"compositionstart",tl),bi(e,"compositionend",nl),bi(e,"change",nl))},mounted(e,{value:t}){e.value=null==t?"":t},beforeUpdate(e,{value:t,modifiers:{lazy:n,trim:o,number:r}},s){if(e._assign=el(s),e.composing)return;if(document.activeElement===e&&"range"!==e.type){if(n)return;if(o&&e.value.trim()===t)return;if((r||"number"===e.type)&&X(e.value)===t)return}const i=null==t?"":t;e.value!==i&&(e.value=i)}},rl={deep:!0,created(e,t,n){e._assign=el(n),bi(e,"change",(()=>{const t=e._modelValue,n=al(e),o=e.checked,r=e._assign;if(E(t)){const e=h(t,n),s=-1!==e;if(o&&!s)r(t.concat(n));else if(!o&&s){const n=[...t];n.splice(e,1),r(n)}}else if(O(t)){const e=new Set(t);o?e.add(n):e.delete(n),r(e)}else r(ul(e,o))}))},mounted:sl,beforeUpdate(e,t,n){e._assign=el(n),sl(e,t,n)}};function sl(e,{value:t,oldValue:n},o){e._modelValue=t,E(t)?e.checked=h(t,o.props.value)>-1:O(t)?e.checked=t.has(o.props.value):t!==n&&(e.checked=d(t,ul(e,!0)))}const il={created(e,{value:t},n){e.checked=d(t,n.props.value),e._assign=el(n),bi(e,"change",(()=>{e._assign(al(e))}))},beforeUpdate(e,{value:t,oldValue:n},o){e._assign=el(o),t!==n&&(e.checked=d(t,o.props.value))}},ll={deep:!0,created(e,{value:t,modifiers:{number:n}},o){const r=O(t);bi(e,"change",(()=>{const t=Array.prototype.filter.call(e.options,(e=>e.selected)).map((e=>n?X(al(e)):al(e)));e._assign(e.multiple?r?new Set(t):t:t[0])})),e._assign=el(o)},mounted(e,{value:t}){cl(e,t)},beforeUpdate(e,t,n){e._assign=el(n)},updated(e,{value:t}){cl(e,t)}};function cl(e,t){const n=e.multiple;if(!n||E(t)||O(t)){for(let o=0,r=e.options.length;o<r;o++){const r=e.options[o],s=al(r);if(n)r.selected=E(t)?h(t,s)>-1:t.has(s);else if(d(al(r),t))return void(e.selectedIndex!==o&&(e.selectedIndex=o))}n||-1===e.selectedIndex||(e.selectedIndex=-1)}}function al(e){return"_value"in e?e._value:e.value}function ul(e,t){const n=t?"_trueValue":"_falseValue";return n in e?e[n]:t}const pl={created(e,t,n){fl(e,t,n,null,"created")},mounted(e,t,n){fl(e,t,n,null,"mounted")},beforeUpdate(e,t,n,o){fl(e,t,n,o,"beforeUpdate")},updated(e,t,n,o){fl(e,t,n,o,"updated")}};function fl(e,t,n,o,r){const s=function(e,t){switch(e){case"SELECT":return ll;case"TEXTAREA":return ol;default:switch(t){case"checkbox":return rl;case"radio":return il;default:return ol}}}(e.tagName,n.props&&n.props.type)[r];s&&s(e,t,n,o)}const dl=["ctrl","shift","alt","meta"],hl={stop:e=>e.stopPropagation(),prevent:e=>e.preventDefault(),self:e=>e.target!==e.currentTarget,ctrl:e=>!e.ctrlKey,shift:e=>!e.shiftKey,alt:e=>!e.altKey,meta:e=>!e.metaKey,left:e=>"button"in e&&0!==e.button,middle:e=>"button"in e&&1!==e.button,right:e=>"button"in e&&2!==e.button,exact:(e,t)=>dl.some((n=>e[`${n}Key`]&&!t.includes(n)))},ml=(e,t)=>(n,...o)=>{for(let e=0;e<t.length;e++){const o=hl[t[e]];if(o&&o(n,t))return}return e(n,...o)},gl={esc:"escape",space:" ",up:"arrow-up",left:"arrow-left",right:"arrow-right",down:"arrow-down",delete:"backspace"},vl=(e,t)=>n=>{if(!("key"in n))return;const o=G(n.key);return t.some((e=>e===o||gl[e]===o))?e(n):void 0},yl={beforeMount(e,{value:t},{transition:n}){e._vod="none"===e.style.display?"":e.style.display,n&&t?n.beforeEnter(e):_l(e,t)},mounted(e,{value:t},{transition:n}){n&&t&&n.enter(e)},updated(e,{value:t,oldValue:n},{transition:o}){!t!=!n&&(o?t?(o.beforeEnter(e),_l(e,!0),o.enter(e)):o.leave(e,(()=>{_l(e,!1)})):_l(e,t))},beforeUnmount(e,{value:t}){_l(e,t)}};function _l(e,t){e.style.display=t?e._vod:"none"}const bl=w({patchProp:(e,t,r,s,i=!1,l,c,a,u)=>{"class"===t?function(e,t,n){const o=e._vtc;o&&(t=(t?[t,...o]:[...o]).join(" ")),null==t?e.removeAttribute("class"):n?e.setAttribute("class",t):e.className=t}(e,s,i):"style"===t?function(e,t,n){const o=e.style,r=P(n);if(n&&!r){for(const e in n)pi(o,e,n[e]);if(t&&!P(t))for(const e in t)null==n[e]&&pi(o,e,"")}else{const s=o.display;r?t!==n&&(o.cssText=n):t&&e.removeAttribute("style"),"_vod"in e&&(o.display=s)}}(e,r,s):x(t)?C(t)||Si(e,t,0,s,c):("."===t[0]?(t=t.slice(1),1):"^"===t[0]?(t=t.slice(1),0):function(e,t,n,o){if(o)return"innerHTML"===t||"textContent"===t||!!(t in e&&Ci.test(t)&&F(n));if("spellcheck"===t||"draggable"===t||"translate"===t)return!1;if("form"===t)return!1;if("list"===t&&"INPUT"===e.tagName)return!1;if("type"===t&&"TEXTAREA"===e.tagName)return!1;if(Ci.test(t)&&P(n))return!1;return t in e}(e,t,s,i))?function(e,t,n,r,s,i,l){if("innerHTML"===t||"textContent"===t)return r&&l(r,s,i),void(e[t]=null==n?"":n);if("value"===t&&"PROGRESS"!==e.tagName&&!e.tagName.includes("-")){e._value=n;const o=null==n?"":n;return e.value===o&&"OPTION"!==e.tagName||(e.value=o),void(null==n&&e.removeAttribute(t))}let c=!1;if(""===n||null==n){const r=typeof e[t];"boolean"===r?n=o(n):null==n&&"string"===r?(n="",c=!0):"number"===r&&(n=0,c=!0)}try{e[t]=n}catch(a){}c&&e.removeAttribute(t)}(e,t,s,l,c,a,u):("true-value"===t?e._trueValue=s:"false-value"===t&&(e._falseValue=s),function(e,t,r,s,i){if(s&&t.startsWith("xlink:"))null==r?e.removeAttributeNS(hi,t.slice(6,t.length)):e.setAttributeNS(hi,t,r);else{const s=n(t);null==r||s&&!o(r)?e.removeAttribute(t):e.setAttribute(t,s?"":r)}}(e,t,s,i))}},ai);let Sl,xl=!1;function Cl(){return Sl||(Sl=Ar(bl))}function wl(){return Sl=xl?Sl:Mr(bl),xl=!0,Sl}const kl=(...e)=>{Cl().render(...e)},Tl=(...e)=>{wl().hydrate(...e)},Nl=(...e)=>{const t=Cl().createApp(...e),{mount:n}=t;return t.mount=e=>{const o=$l(e);if(!o)return;const r=t._component;F(r)||r.render||r.template||(r.template=o.innerHTML),o.innerHTML="";const s=n(o,!1,o instanceof SVGElement);return o instanceof Element&&(o.removeAttribute("v-cloak"),o.setAttribute("data-v-app","")),s},t},El=(...e)=>{const t=wl().createApp(...e),{mount:n}=t;return t.mount=e=>{const t=$l(e);if(t)return n(t,!0,t instanceof SVGElement)},t};function $l(e){if(P(e)){return document.querySelector(e)}return e}const Ol=_;var Rl=Object.freeze({__proto__:null,render:kl,hydrate:Tl,createApp:Nl,createSSRApp:El,initDirectivesForSSR:Ol,defineCustomElement:wi,defineSSRCustomElement:ki,VueElement:Ni,useCssModule:Ei,useCssVars:$i,Transition:Fi,TransitionGroup:Yi,vModelText:ol,vModelCheckbox:rl,vModelRadio:il,vModelSelect:ll,vModelDynamic:pl,withModifiers:ml,withKeys:vl,vShow:yl,reactive:gt,ref:Ft,readonly:yt,unref:It,proxyRefs:Lt,isRef:Rt,toRef:Wt,toRefs:Dt,isProxy:wt,isReactive:St,isReadonly:xt,isShallow:Ct,customRef:Ut,triggerRef:Vt,shallowRef:Pt,shallowReactive:vt,shallowReadonly:_t,markRaw:Tt,toRaw:kt,effect:ye,stop:_e,ReactiveEffect:ge,effectScope:oe,EffectScope:ne,getCurrentScope:se,onScopeDispose:ie,computed:js,watch:Xn,watchEffect:Jn,watchPostEffect:Yn,watchSyncEffect:Zn,onBeforeMount:$o,onMounted:Oo,onBeforeUpdate:Ro,onUpdated:Fo,onBeforeUnmount:Po,onUnmounted:Ao,onActivated:So,onDeactivated:xo,onRenderTracked:Io,onRenderTriggered:Vo,onErrorCaptured:Bo,onServerPrefetch:Mo,provide:Gn,inject:qn,nextTick:dn,defineComponent:ho,defineAsyncComponent:go,useAttrs:Ks,useSlots:zs,defineProps:Us,defineEmits:Ds,defineExpose:Hs,withDefaults:Ws,mergeDefaults:qs,createPropsRestProxy:Js,withAsyncContext:Ys,getCurrentInstance:ws,h:Zs,createVNode:us,cloneVNode:fs,mergeProps:_s,isVNode:os,Fragment:Wr,Text:zr,Comment:Kr,Static:Gr,Teleport:Hr,Suspense:Un,KeepAlive:_o,BaseTransition:io,withDirectives:Lo,useSSRContext:Xs,ssrContextKey:Qs,createRenderer:Ar,createHydrationRenderer:Mr,queuePostFlushCb:vn,warn:Gt,handleError:Qt,callWithErrorHandling:Yt,callWithAsyncErrorHandling:Zt,resolveComponent:Uo,resolveDirective:Wo,resolveDynamicComponent:Ho,registerRuntimeCompiler:Fs,isRuntimeOnly:Ps,useTransitionState:ro,resolveTransitionHooks:co,setTransitionHooks:po,getTransitionRawChildren:fo,initCustomFormatter:ei,get devtools(){return xn},setDevtoolsHook:wn,withCtx:An,pushScopeId:Rn,popScopeId:Fn,withScopeId:Pn,renderList:Go,toHandlers:Zo,renderSlot:Jo,createSlots:qo,withMemo:ti,isMemoSame:ni,openBlock:Yr,createBlock:ns,setBlockTracking:Xr,createTextVNode:ds,createCommentVNode:ms,createStaticVNode:hs,createElementVNode:as,createElementBlock:ts,guardReactiveProps:ps,toDisplayString:m,camelize:z,capitalize:q,toHandlerKey:J,normalizeProps:a,normalizeClass:c,normalizeStyle:r,transformVNodeArgs:ss,version:oi,ssrUtils:null,resolveFilter:null,compatUtils:null});function Fl(e){throw e}function Pl(e){}function Al(e,t,n,o){const r=new SyntaxError(String(e));return r.code=e,r.loc=t,r}const Ml=Symbol(""),Vl=Symbol(""),Il=Symbol(""),Bl=Symbol(""),Ll=Symbol(""),jl=Symbol(""),Ul=Symbol(""),Dl=Symbol(""),Hl=Symbol(""),Wl=Symbol(""),zl=Symbol(""),Kl=Symbol(""),Gl=Symbol(""),ql=Symbol(""),Jl=Symbol(""),Yl=Symbol(""),Zl=Symbol(""),Ql=Symbol(""),Xl=Symbol(""),ec=Symbol(""),tc=Symbol(""),nc=Symbol(""),oc=Symbol(""),rc=Symbol(""),sc=Symbol(""),ic=Symbol(""),lc=Symbol(""),cc=Symbol(""),ac=Symbol(""),uc=Symbol(""),pc=Symbol(""),fc=Symbol(""),dc=Symbol(""),hc=Symbol(""),mc=Symbol(""),gc=Symbol(""),vc=Symbol(""),yc=Symbol(""),_c=Symbol(""),bc={[Ml]:"Fragment",[Vl]:"Teleport",[Il]:"Suspense",[Bl]:"KeepAlive",[Ll]:"BaseTransition",[jl]:"openBlock",[Ul]:"createBlock",[Dl]:"createElementBlock",[Hl]:"createVNode",[Wl]:"createElementVNode",[zl]:"createCommentVNode",[Kl]:"createTextVNode",[Gl]:"createStaticVNode",[ql]:"resolveComponent",[Jl]:"resolveDynamicComponent",[Yl]:"resolveDirective",[Zl]:"resolveFilter",[Ql]:"withDirectives",[Xl]:"renderList",[ec]:"renderSlot",[tc]:"createSlots",[nc]:"toDisplayString",[oc]:"mergeProps",[rc]:"normalizeClass",[sc]:"normalizeStyle",[ic]:"normalizeProps",[lc]:"guardReactiveProps",[cc]:"toHandlers",[ac]:"camelize",[uc]:"capitalize",[pc]:"toHandlerKey",[fc]:"setBlockTracking",[dc]:"pushScopeId",[hc]:"popScopeId",[mc]:"withCtx",[gc]:"unref",[vc]:"isRef",[yc]:"withMemo",[_c]:"isMemoSame"};const Sc={source:"",start:{line:1,column:1,offset:0},end:{line:1,column:1,offset:0}};function xc(e,t,n,o,r,s,i,l=!1,c=!1,a=!1,u=Sc){return e&&(l?(e.helper(jl),e.helper(Zc(e.inSSR,a))):e.helper(Yc(e.inSSR,a)),i&&e.helper(Ql)),{type:13,tag:t,props:n,children:o,patchFlag:r,dynamicProps:s,directives:i,isBlock:l,disableTracking:c,isComponent:a,loc:u}}function Cc(e,t=Sc){return{type:17,loc:t,elements:e}}function wc(e,t=Sc){return{type:15,loc:t,properties:e}}function kc(e,t){return{type:16,loc:Sc,key:P(e)?Tc(e,!0):e,value:t}}function Tc(e,t=!1,n=Sc,o=0){return{type:4,loc:n,content:e,isStatic:t,constType:t?3:o}}function Nc(e,t=Sc){return{type:8,loc:t,children:e}}function Ec(e,t=[],n=Sc){return{type:14,loc:n,callee:e,arguments:t}}function $c(e,t,n=!1,o=!1,r=Sc){return{type:18,params:e,returns:t,newline:n,isSlot:o,loc:r}}function Oc(e,t,n,o=!0){return{type:19,test:e,consequent:t,alternate:n,newline:o,loc:Sc}}const Rc=e=>4===e.type&&e.isStatic,Fc=(e,t)=>e===t||e===G(t);function Pc(e){return Fc(e,"Teleport")?Vl:Fc(e,"Suspense")?Il:Fc(e,"KeepAlive")?Bl:Fc(e,"BaseTransition")?Ll:void 0}const Ac=/^\d|[^\$\w]/,Mc=e=>!Ac.test(e),Vc=/[A-Za-z_$\xA0-\uFFFF]/,Ic=/[\.\?\w$\xA0-\uFFFF]/,Bc=/\s+[.[]\s*|\s*[.[]\s+/g,Lc=e=>{e=e.trim().replace(Bc,(e=>e.trim()));let t=0,n=[],o=0,r=0,s=null;for(let i=0;i<e.length;i++){const l=e.charAt(i);switch(t){case 0:if("["===l)n.push(t),t=1,o++;else if("("===l)n.push(t),t=2,r++;else if(!(0===i?Vc:Ic).test(l))return!1;break;case 1:"'"===l||'"'===l||"`"===l?(n.push(t),t=3,s=l):"["===l?o++:"]"===l&&(--o||(t=n.pop()));break;case 2:if("'"===l||'"'===l||"`"===l)n.push(t),t=3,s=l;else if("("===l)r++;else if(")"===l){if(i===e.length-1)return!1;--r||(t=n.pop())}break;case 3:l===s&&(t=n.pop(),s=null)}}return!o&&!r};function jc(e,t,n){const o={source:e.source.slice(t,t+n),start:Uc(e.start,e.source,t),end:e.end};return null!=n&&(o.end=Uc(e.start,e.source,t+n)),o}function Uc(e,t,n=t.length){return Dc(w({},e),t,n)}function Dc(e,t,n=t.length){let o=0,r=-1;for(let s=0;s<n;s++)10===t.charCodeAt(s)&&(o++,r=s);return e.offset+=n,e.line+=o,e.column=-1===r?e.column+n:n-r,e}function Hc(e,t,n=!1){for(let o=0;o<e.props.length;o++){const r=e.props[o];if(7===r.type&&(n||r.exp)&&(P(t)?r.name===t:t.test(r.name)))return r}}function Wc(e,t,n=!1,o=!1){for(let r=0;r<e.props.length;r++){const s=e.props[r];if(6===s.type){if(n)continue;if(s.name===t&&(s.value||o))return s}else if("bind"===s.name&&(s.exp||o)&&zc(s.arg,t))return s}}function zc(e,t){return!(!e||!Rc(e)||e.content!==t)}function Kc(e){return 5===e.type||2===e.type}function Gc(e){return 7===e.type&&"slot"===e.name}function qc(e){return 1===e.type&&3===e.tagType}function Jc(e){return 1===e.type&&2===e.tagType}function Yc(e,t){return e||t?Hl:Wl}function Zc(e,t){return e||t?Ul:Dl}const Qc=new Set([ic,lc]);function Xc(e,t=[]){if(e&&!P(e)&&14===e.type){const n=e.callee;if(!P(n)&&Qc.has(n))return Xc(e.arguments[0],t.concat(e))}return[e,t]}function ea(e,t,n){let o,r,s=13===e.type?e.props:e.arguments[2],i=[];if(s&&!P(s)&&14===s.type){const e=Xc(s);s=e[0],i=e[1],r=i[i.length-1]}if(null==s||P(s))o=wc([t]);else if(14===s.type){const e=s.arguments[0];P(e)||15!==e.type?s.callee===cc?o=Ec(n.helper(oc),[wc([t]),s]):s.arguments.unshift(wc([t])):e.properties.unshift(t),!o&&(o=s)}else if(15===s.type){let e=!1;if(4===t.key.type){const n=t.key.content;e=s.properties.some((e=>4===e.key.type&&e.key.content===n))}e||s.properties.unshift(t),o=s}else o=Ec(n.helper(oc),[wc([t]),s]),r&&r.callee===lc&&(r=i[i.length-2]);13===e.type?r?r.arguments[0]=o:e.props=o:r?r.arguments[0]=o:e.arguments[2]=o}function ta(e,t){return`_${t}_${e.replace(/[^\w]/g,((t,n)=>"-"===t?"_":e.charCodeAt(n).toString()))}`}function na(e,{helper:t,removeHelper:n,inSSR:o}){e.isBlock||(e.isBlock=!0,n(Yc(o,e.isComponent)),t(jl),t(Zc(o,e.isComponent)))}const oa=/&(gt|lt|amp|apos|quot);/g,ra={gt:">",lt:"<",amp:"&",apos:"'",quot:'"'},sa={delimiters:["{{","}}"],getNamespace:()=>0,getTextMode:()=>0,isVoidTag:b,isPreTag:b,isCustomElement:b,decodeEntities:e=>e.replace(oa,((e,t)=>ra[t])),onError:Fl,onWarn:Pl,comments:!1};function ia(e,t={}){const n=function(e,t){const n=w({},sa);let o;for(o in t)n[o]=void 0===t[o]?sa[o]:t[o];return{options:n,column:1,line:1,offset:0,originalSource:e,source:e,inPre:!1,inVPre:!1,onWarn:n.onWarn}}(e,t),o=ba(n);return function(e,t=Sc){return{type:0,children:e,helpers:[],components:[],directives:[],hoists:[],imports:[],cached:0,temps:0,codegenNode:void 0,loc:t}}(la(n,0,[]),Sa(n,o))}function la(e,t,n){const o=xa(n),r=o?o.ns:0,s=[];for(;!Na(e,t,n);){const i=e.source;let l;if(0===t||1===t)if(!e.inVPre&&Ca(i,e.options.delimiters[0]))l=va(e,t);else if(0===t&&"<"===i[0])if(1===i.length);else if("!"===i[1])l=Ca(i,"\x3c!--")?ua(e):Ca(i,"<!DOCTYPE")?pa(e):Ca(i,"<![CDATA[")&&0!==r?aa(e,n):pa(e);else if("/"===i[1])if(2===i.length);else{if(">"===i[2]){wa(e,3);continue}if(/[a-z]/i.test(i[2])){ha(e,1,o);continue}l=pa(e)}else/[a-z]/i.test(i[1])?l=fa(e,n):"?"===i[1]&&(l=pa(e));if(l||(l=ya(e,t)),E(l))for(let e=0;e<l.length;e++)ca(s,l[e]);else ca(s,l)}let i=!1;if(2!==t&&1!==t){const t="preserve"!==e.options.whitespace;for(let n=0;n<s.length;n++){const o=s[n];if(e.inPre||2!==o.type)3!==o.type||e.options.comments||(i=!0,s[n]=null);else if(/[^\t\r\n\f ]/.test(o.content))t&&(o.content=o.content.replace(/[\t\r\n\f ]+/g," "));else{const e=s[n-1],r=s[n+1];!e||!r||t&&(3===e.type||3===r.type||1===e.type&&1===r.type&&/[\r\n]/.test(o.content))?(i=!0,s[n]=null):o.content=" "}}if(e.inPre&&o&&e.options.isPreTag(o.tag)){const e=s[0];e&&2===e.type&&(e.content=e.content.replace(/^\r?\n/,""))}}return i?s.filter(Boolean):s}function ca(e,t){if(2===t.type){const n=xa(e);if(n&&2===n.type&&n.loc.end.offset===t.loc.start.offset)return n.content+=t.content,n.loc.end=t.loc.end,void(n.loc.source+=t.loc.source)}e.push(t)}function aa(e,t){wa(e,9);const n=la(e,3,t);return 0===e.source.length||wa(e,3),n}function ua(e){const t=ba(e);let n;const o=/--(\!)?>/.exec(e.source);if(o){n=e.source.slice(4,o.index);const t=e.source.slice(0,o.index);let r=1,s=0;for(;-1!==(s=t.indexOf("\x3c!--",r));)wa(e,s-r+1),r=s+1;wa(e,o.index+o[0].length-r+1)}else n=e.source.slice(4),wa(e,e.source.length);return{type:3,content:n,loc:Sa(e,t)}}function pa(e){const t=ba(e),n="?"===e.source[1]?1:2;let o;const r=e.source.indexOf(">");return-1===r?(o=e.source.slice(n),wa(e,e.source.length)):(o=e.source.slice(n,r),wa(e,r+1)),{type:3,content:o,loc:Sa(e,t)}}function fa(e,t){const n=e.inPre,o=e.inVPre,r=xa(t),s=ha(e,0,r),i=e.inPre&&!n,l=e.inVPre&&!o;if(s.isSelfClosing||e.options.isVoidTag(s.tag))return i&&(e.inPre=!1),l&&(e.inVPre=!1),s;t.push(s);const c=e.options.getTextMode(s,r),a=la(e,c,t);if(t.pop(),s.children=a,Ea(e.source,s.tag))ha(e,1,r);else if(0===e.source.length&&"script"===s.tag.toLowerCase()){const e=a[0];e&&Ca(e.loc.source,"\x3c!--")}return s.loc=Sa(e,s.loc.start),i&&(e.inPre=!1),l&&(e.inVPre=!1),s}const da=e("if,else,else-if,for,slot");function ha(e,t,n){const o=ba(e),r=/^<\/?([a-z][^\t\r\n\f />]*)/i.exec(e.source),s=r[1],i=e.options.getNamespace(s,n);wa(e,r[0].length),ka(e);const l=ba(e),c=e.source;e.options.isPreTag(s)&&(e.inPre=!0);let a=ma(e,t);0===t&&!e.inVPre&&a.some((e=>7===e.type&&"pre"===e.name))&&(e.inVPre=!0,w(e,l),e.source=c,a=ma(e,t).filter((e=>"v-pre"!==e.name)));let u=!1;if(0===e.source.length||(u=Ca(e.source,"/>"),wa(e,u?2:1)),1===t)return;let p=0;return e.inVPre||("slot"===s?p=2:"template"===s?a.some((e=>7===e.type&&da(e.name)))&&(p=3):function(e,t,n){const o=n.options;if(o.isCustomElement(e))return!1;if("component"===e||/^[A-Z]/.test(e)||Pc(e)||o.isBuiltInComponent&&o.isBuiltInComponent(e)||o.isNativeTag&&!o.isNativeTag(e))return!0;for(let r=0;r<t.length;r++){const e=t[r];if(6===e.type){if("is"===e.name&&e.value&&e.value.content.startsWith("vue:"))return!0}else{if("is"===e.name)return!0;"bind"===e.name&&zc(e.arg,"is")}}}(s,a,e)&&(p=1)),{type:1,ns:i,tag:s,tagType:p,props:a,isSelfClosing:u,children:[],loc:Sa(e,o),codegenNode:void 0}}function ma(e,t){const n=[],o=new Set;for(;e.source.length>0&&!Ca(e.source,">")&&!Ca(e.source,"/>");){if(Ca(e.source,"/")){wa(e,1),ka(e);continue}const r=ga(e,o);6===r.type&&r.value&&"class"===r.name&&(r.value.content=r.value.content.replace(/\s+/g," ").trim()),0===t&&n.push(r),/^[^\t\r\n\f />]/.test(e.source),ka(e)}return n}function ga(e,t){const n=ba(e),o=/^[^\t\r\n\f />][^\t\r\n\f />=]*/.exec(e.source)[0];t.has(o),t.add(o);{const e=/["'<]/g;let t;for(;t=e.exec(o););}let r;wa(e,o.length),/^[\t\r\n\f ]*=/.test(e.source)&&(ka(e),wa(e,1),ka(e),r=function(e){const t=ba(e);let n;const o=e.source[0],r='"'===o||"'"===o;if(r){wa(e,1);const t=e.source.indexOf(o);-1===t?n=_a(e,e.source.length,4):(n=_a(e,t,4),wa(e,1))}else{const t=/^[^\t\r\n\f >]+/.exec(e.source);if(!t)return;const o=/["'<=`]/g;let r;for(;r=o.exec(t[0]););n=_a(e,t[0].length,4)}return{content:n,isQuoted:r,loc:Sa(e,t)}}(e));const s=Sa(e,n);if(!e.inVPre&&/^(v-[A-Za-z0-9-]|:|\.|@|#)/.test(o)){const t=/(?:^v-([a-z0-9-]+))?(?:(?::|^\.|^@|^#)(\[[^\]]+\]|[^\.]+))?(.+)?$/i.exec(o);let i,l=Ca(o,"."),c=t[1]||(l||Ca(o,":")?"bind":Ca(o,"@")?"on":"slot");if(t[2]){const r="slot"===c,s=o.lastIndexOf(t[2]),l=Sa(e,Ta(e,n,s),Ta(e,n,s+t[2].length+(r&&t[3]||"").length));let a=t[2],u=!0;a.startsWith("[")?(u=!1,a=a.endsWith("]")?a.slice(1,a.length-1):a.slice(1)):r&&(a+=t[3]||""),i={type:4,content:a,isStatic:u,constType:u?3:0,loc:l}}if(r&&r.isQuoted){const e=r.loc;e.start.offset++,e.start.column++,e.end=Uc(e.start,r.content),e.source=e.source.slice(1,-1)}const a=t[3]?t[3].slice(1).split("."):[];return l&&a.push("prop"),{type:7,name:c,exp:r&&{type:4,content:r.content,isStatic:!1,constType:0,loc:r.loc},arg:i,modifiers:a,loc:s}}return!e.inVPre&&Ca(o,"v-"),{type:6,name:o,value:r&&{type:2,content:r.content,loc:r.loc},loc:s}}function va(e,t){const[n,o]=e.options.delimiters,r=e.source.indexOf(o,n.length);if(-1===r)return;const s=ba(e);wa(e,n.length);const i=ba(e),l=ba(e),c=r-n.length,a=e.source.slice(0,c),u=_a(e,c,t),p=u.trim(),f=u.indexOf(p);f>0&&Dc(i,a,f);return Dc(l,a,c-(u.length-p.length-f)),wa(e,o.length),{type:5,content:{type:4,isStatic:!1,constType:0,content:p,loc:Sa(e,i,l)},loc:Sa(e,s)}}function ya(e,t){const n=3===t?["]]>"]:["<",e.options.delimiters[0]];let o=e.source.length;for(let s=0;s<n.length;s++){const t=e.source.indexOf(n[s],1);-1!==t&&o>t&&(o=t)}const r=ba(e);return{type:2,content:_a(e,o,t),loc:Sa(e,r)}}function _a(e,t,n){const o=e.source.slice(0,t);return wa(e,t),2!==n&&3!==n&&o.includes("&")?e.options.decodeEntities(o,4===n):o}function ba(e){const{column:t,line:n,offset:o}=e;return{column:t,line:n,offset:o}}function Sa(e,t,n){return{start:t,end:n=n||ba(e),source:e.originalSource.slice(t.offset,n.offset)}}function xa(e){return e[e.length-1]}function Ca(e,t){return e.startsWith(t)}function wa(e,t){const{source:n}=e;Dc(e,n,t),e.source=n.slice(t)}function ka(e){const t=/^[\t\r\n\f ]+/.exec(e.source);t&&wa(e,t[0].length)}function Ta(e,t,n){return Uc(t,e.originalSource.slice(t.offset,n),n)}function Na(e,t,n){const o=e.source;switch(t){case 0:if(Ca(o,"</"))for(let e=n.length-1;e>=0;--e)if(Ea(o,n[e].tag))return!0;break;case 1:case 2:{const e=xa(n);if(e&&Ea(o,e.tag))return!0;break}case 3:if(Ca(o,"]]>"))return!0}return!o}function Ea(e,t){return Ca(e,"</")&&e.slice(2,2+t.length).toLowerCase()===t.toLowerCase()&&/[\t\r\n\f />]/.test(e[2+t.length]||">")}function $a(e,t){Ra(e,t,Oa(e,e.children[0]))}function Oa(e,t){const{children:n}=e;return 1===n.length&&1===t.type&&!Jc(t)}function Ra(e,t,n=!1){const{children:o}=e,r=o.length;let s=0;for(let i=0;i<o.length;i++){const e=o[i];if(1===e.type&&0===e.tagType){const o=n?0:Fa(e,t);if(o>0){if(o>=2){e.codegenNode.patchFlag="-1",e.codegenNode=t.hoist(e.codegenNode),s++;continue}}else{const n=e.codegenNode;if(13===n.type){const o=Ia(n);if((!o||512===o||1===o)&&Ma(e,t)>=2){const o=Va(e);o&&(n.props=t.hoist(o))}n.dynamicProps&&(n.dynamicProps=t.hoist(n.dynamicProps))}}}else 12===e.type&&Fa(e.content,t)>=2&&(e.codegenNode=t.hoist(e.codegenNode),s++);if(1===e.type){const n=1===e.tagType;n&&t.scopes.vSlot++,Ra(e,t),n&&t.scopes.vSlot--}else if(11===e.type)Ra(e,t,1===e.children.length);else if(9===e.type)for(let n=0;n<e.branches.length;n++)Ra(e.branches[n],t,1===e.branches[n].children.length)}s&&t.transformHoist&&t.transformHoist(o,t,e),s&&s===r&&1===e.type&&0===e.tagType&&e.codegenNode&&13===e.codegenNode.type&&E(e.codegenNode.children)&&(e.codegenNode.children=t.hoist(Cc(e.codegenNode.children)))}function Fa(e,t){const{constantCache:n}=t;switch(e.type){case 1:if(0!==e.tagType)return 0;const o=n.get(e);if(void 0!==o)return o;const r=e.codegenNode;if(13!==r.type)return 0;if(r.isBlock&&"svg"!==e.tag&&"foreignObject"!==e.tag)return 0;if(Ia(r))return n.set(e,0),0;{let o=3;const s=Ma(e,t);if(0===s)return n.set(e,0),0;s<o&&(o=s);for(let r=0;r<e.children.length;r++){const s=Fa(e.children[r],t);if(0===s)return n.set(e,0),0;s<o&&(o=s)}if(o>1)for(let r=0;r<e.props.length;r++){const s=e.props[r];if(7===s.type&&"bind"===s.name&&s.exp){const r=Fa(s.exp,t);if(0===r)return n.set(e,0),0;r<o&&(o=r)}}if(r.isBlock){for(let t=0;t<e.props.length;t++){if(7===e.props[t].type)return n.set(e,0),0}t.removeHelper(jl),t.removeHelper(Zc(t.inSSR,r.isComponent)),r.isBlock=!1,t.helper(Yc(t.inSSR,r.isComponent))}return n.set(e,o),o}case 2:case 3:return 3;case 9:case 11:case 10:default:return 0;case 5:case 12:return Fa(e.content,t);case 4:return e.constType;case 8:let s=3;for(let n=0;n<e.children.length;n++){const o=e.children[n];if(P(o)||A(o))continue;const r=Fa(o,t);if(0===r)return 0;r<s&&(s=r)}return s}}const Pa=new Set([rc,sc,ic,lc]);function Aa(e,t){if(14===e.type&&!P(e.callee)&&Pa.has(e.callee)){const n=e.arguments[0];if(4===n.type)return Fa(n,t);if(14===n.type)return Aa(n,t)}return 0}function Ma(e,t){let n=3;const o=Va(e);if(o&&15===o.type){const{properties:e}=o;for(let o=0;o<e.length;o++){const{key:r,value:s}=e[o],i=Fa(r,t);if(0===i)return i;let l;if(i<n&&(n=i),l=4===s.type?Fa(s,t):14===s.type?Aa(s,t):0,0===l)return l;l<n&&(n=l)}}return n}function Va(e){const t=e.codegenNode;if(13===t.type)return t.props}function Ia(e){const t=e.patchFlag;return t?parseInt(t,10):void 0}function Ba(e,{filename:t="",prefixIdentifiers:n=!1,hoistStatic:o=!1,cacheHandlers:r=!1,nodeTransforms:s=[],directiveTransforms:i={},transformHoist:l=null,isBuiltInComponent:c=_,isCustomElement:a=_,expressionPlugins:u=[],scopeId:p=null,slotted:f=!0,ssr:d=!1,inSSR:h=!1,ssrCssVars:m="",bindingMetadata:g=v,inline:y=!1,isTS:b=!1,onError:S=Fl,onWarn:x=Pl,compatConfig:C}){const w=t.replace(/\?.*$/,"").match(/([^/\\]+)\.\w+$/),k={selfName:w&&q(z(w[1])),prefixIdentifiers:n,hoistStatic:o,cacheHandlers:r,nodeTransforms:s,directiveTransforms:i,transformHoist:l,isBuiltInComponent:c,isCustomElement:a,expressionPlugins:u,scopeId:p,slotted:f,ssr:d,inSSR:h,ssrCssVars:m,bindingMetadata:g,inline:y,isTS:b,onError:S,onWarn:x,compatConfig:C,root:e,helpers:new Map,components:new Set,directives:new Set,hoists:[],imports:[],constantCache:new Map,temps:0,cached:0,identifiers:Object.create(null),scopes:{vFor:0,vSlot:0,vPre:0,vOnce:0},parent:null,currentNode:e,childIndex:0,inVOnce:!1,helper(e){const t=k.helpers.get(e)||0;return k.helpers.set(e,t+1),e},removeHelper(e){const t=k.helpers.get(e);if(t){const n=t-1;n?k.helpers.set(e,n):k.helpers.delete(e)}},helperString:e=>`_${bc[k.helper(e)]}`,replaceNode(e){k.parent.children[k.childIndex]=k.currentNode=e},removeNode(e){const t=e?k.parent.children.indexOf(e):k.currentNode?k.childIndex:-1;e&&e!==k.currentNode?k.childIndex>t&&(k.childIndex--,k.onNodeRemoved()):(k.currentNode=null,k.onNodeRemoved()),k.parent.children.splice(t,1)},onNodeRemoved:()=>{},addIdentifiers(e){},removeIdentifiers(e){},hoist(e){P(e)&&(e=Tc(e)),k.hoists.push(e);const t=Tc(`_hoisted_${k.hoists.length}`,!1,e.loc,2);return t.hoisted=e,t},cache:(e,t=!1)=>function(e,t,n=!1){return{type:20,index:e,value:t,isVNode:n,loc:Sc}}(k.cached++,e,t)};return k}function La(e,t){const n=Ba(e,t);ja(e,n),t.hoistStatic&&$a(e,n),t.ssr||function(e,t){const{helper:n}=t,{children:o}=e;if(1===o.length){const n=o[0];if(Oa(e,n)&&n.codegenNode){const o=n.codegenNode;13===o.type&&na(o,t),e.codegenNode=o}else e.codegenNode=n}else if(o.length>1){let o=64;e.codegenNode=xc(t,n(Ml),void 0,e.children,o+"",void 0,void 0,!0,void 0,!1)}}(e,n),e.helpers=[...n.helpers.keys()],e.components=[...n.components],e.directives=[...n.directives],e.imports=n.imports,e.hoists=n.hoists,e.temps=n.temps,e.cached=n.cached}function ja(e,t){t.currentNode=e;const{nodeTransforms:n}=t,o=[];for(let s=0;s<n.length;s++){const r=n[s](e,t);if(r&&(E(r)?o.push(...r):o.push(r)),!t.currentNode)return;e=t.currentNode}switch(e.type){case 3:t.ssr||t.helper(zl);break;case 5:t.ssr||t.helper(nc);break;case 9:for(let n=0;n<e.branches.length;n++)ja(e.branches[n],t);break;case 10:case 11:case 1:case 0:!function(e,t){let n=0;const o=()=>{n--};for(;n<e.children.length;n++){const r=e.children[n];P(r)||(t.parent=e,t.childIndex=n,t.onNodeRemoved=o,ja(r,t))}}(e,t)}t.currentNode=e;let r=o.length;for(;r--;)o[r]()}function Ua(e,t){const n=P(e)?t=>t===e:t=>e.test(t);return(e,o)=>{if(1===e.type){const{props:r}=e;if(3===e.tagType&&r.some(Gc))return;const s=[];for(let i=0;i<r.length;i++){const l=r[i];if(7===l.type&&n(l.name)){r.splice(i,1),i--;const n=t(e,l,o);n&&s.push(n)}}return s}}}const Da=e=>`${bc[e]}: _${bc[e]}`;function Ha(e,t={}){const n=function(e,{mode:t="function",prefixIdentifiers:n="module"===t,sourceMap:o=!1,filename:r="template.vue.html",scopeId:s=null,optimizeImports:i=!1,runtimeGlobalName:l="Vue",runtimeModuleName:c="vue",ssrRuntimeModuleName:a="vue/server-renderer",ssr:u=!1,isTS:p=!1,inSSR:f=!1}){const d={mode:t,prefixIdentifiers:n,sourceMap:o,filename:r,scopeId:s,optimizeImports:i,runtimeGlobalName:l,runtimeModuleName:c,ssrRuntimeModuleName:a,ssr:u,isTS:p,inSSR:f,source:e.loc.source,code:"",column:1,line:1,offset:0,indentLevel:0,pure:!1,map:void 0,helper:e=>`_${bc[e]}`,push(e,t){d.code+=e},indent(){h(++d.indentLevel)},deindent(e=!1){e?--d.indentLevel:h(--d.indentLevel)},newline(){h(d.indentLevel)}};function h(e){d.push("\n"+" ".repeat(e))}return d}(e,t);t.onContextCreated&&t.onContextCreated(n);const{mode:o,push:r,prefixIdentifiers:s,indent:i,deindent:l,newline:c,ssr:a}=n,u=e.helpers.length>0,p=!s&&"module"!==o;!function(e,t){const{push:n,newline:o,runtimeGlobalName:r}=t,s=r;if(e.helpers.length>0&&(n(`const _Vue = ${s}\n`),e.hoists.length)){n(`const { ${[Hl,Wl,zl,Kl,Gl].filter((t=>e.helpers.includes(t))).map(Da).join(", ")} } = _Vue\n`)}(function(e,t){if(!e.length)return;t.pure=!0;const{push:n,newline:o}=t;o();for(let r=0;r<e.length;r++){const s=e[r];s&&(n(`const _hoisted_${r+1} = `),Ga(s,t),o())}t.pure=!1})(e.hoists,t),o(),n("return ")}(e,n);if(r(`function ${a?"ssrRender":"render"}(${(a?["_ctx","_push","_parent","_attrs"]:["_ctx","_cache"]).join(", ")}) {`),i(),p&&(r("with (_ctx) {"),i(),u&&(r(`const { ${e.helpers.map(Da).join(", ")} } = _Vue`),r("\n"),c())),e.components.length&&(Wa(e.components,"component",n),(e.directives.length||e.temps>0)&&c()),e.directives.length&&(Wa(e.directives,"directive",n),e.temps>0&&c()),e.temps>0){r("let ");for(let t=0;t<e.temps;t++)r(`${t>0?", ":""}_temp${t}`)}return(e.components.length||e.directives.length||e.temps)&&(r("\n"),c()),a||r("return "),e.codegenNode?Ga(e.codegenNode,n):r("null"),p&&(l(),r("}")),l(),r("}"),{ast:e,code:n.code,preamble:"",map:n.map?n.map.toJSON():void 0}}function Wa(e,t,{helper:n,push:o,newline:r,isTS:s}){const i=n("component"===t?ql:Yl);for(let l=0;l<e.length;l++){let n=e[l];const c=n.endsWith("__self");c&&(n=n.slice(0,-6)),o(`const ${ta(n,t)} = ${i}(${JSON.stringify(n)}${c?", true":""})${s?"!":""}`),l<e.length-1&&r()}}function za(e,t){const n=e.length>3||!1;t.push("["),n&&t.indent(),Ka(e,t,n),n&&t.deindent(),t.push("]")}function Ka(e,t,n=!1,o=!0){const{push:r,newline:s}=t;for(let i=0;i<e.length;i++){const l=e[i];P(l)?r(l):E(l)?za(l,t):Ga(l,t),i<e.length-1&&(n?(o&&r(","),s()):o&&r(", "))}}function Ga(e,t){if(P(e))t.push(e);else if(A(e))t.push(t.helper(e));else switch(e.type){case 1:case 9:case 11:case 12:Ga(e.codegenNode,t);break;case 2:!function(e,t){t.push(JSON.stringify(e.content),e)}(e,t);break;case 4:qa(e,t);break;case 5:!function(e,t){const{push:n,helper:o,pure:r}=t;r&&n("/*#__PURE__*/");n(`${o(nc)}(`),Ga(e.content,t),n(")")}(e,t);break;case 8:Ja(e,t);break;case 3:!function(e,t){const{push:n,helper:o,pure:r}=t;r&&n("/*#__PURE__*/");n(`${o(zl)}(${JSON.stringify(e.content)})`,e)}(e,t);break;case 13:!function(e,t){const{push:n,helper:o,pure:r}=t,{tag:s,props:i,children:l,patchFlag:c,dynamicProps:a,directives:u,isBlock:p,disableTracking:f,isComponent:d}=e;u&&n(o(Ql)+"(");p&&n(`(${o(jl)}(${f?"true":""}), `);r&&n("/*#__PURE__*/");const h=p?Zc(t.inSSR,d):Yc(t.inSSR,d);n(o(h)+"(",e),Ka(function(e){let t=e.length;for(;t--&&null==e[t];);return e.slice(0,t+1).map((e=>e||"null"))}([s,i,l,c,a]),t),n(")"),p&&n(")");u&&(n(", "),Ga(u,t),n(")"))}(e,t);break;case 14:!function(e,t){const{push:n,helper:o,pure:r}=t,s=P(e.callee)?e.callee:o(e.callee);r&&n("/*#__PURE__*/");n(s+"(",e),Ka(e.arguments,t),n(")")}(e,t);break;case 15:!function(e,t){const{push:n,indent:o,deindent:r,newline:s}=t,{properties:i}=e;if(!i.length)return void n("{}",e);const l=i.length>1||!1;n(l?"{":"{ "),l&&o();for(let c=0;c<i.length;c++){const{key:e,value:o}=i[c];Ya(e,t),n(": "),Ga(o,t),c<i.length-1&&(n(","),s())}l&&r(),n(l?"}":" }")}(e,t);break;case 17:!function(e,t){za(e.elements,t)}(e,t);break;case 18:!function(e,t){const{push:n,indent:o,deindent:r}=t,{params:s,returns:i,body:l,newline:c,isSlot:a}=e;a&&n(`_${bc[mc]}(`);n("(",e),E(s)?Ka(s,t):s&&Ga(s,t);n(") => "),(c||l)&&(n("{"),o());i?(c&&n("return "),E(i)?za(i,t):Ga(i,t)):l&&Ga(l,t);(c||l)&&(r(),n("}"));a&&n(")")}(e,t);break;case 19:!function(e,t){const{test:n,consequent:o,alternate:r,newline:s}=e,{push:i,indent:l,deindent:c,newline:a}=t;if(4===n.type){const e=!Mc(n.content);e&&i("("),qa(n,t),e&&i(")")}else i("("),Ga(n,t),i(")");s&&l(),t.indentLevel++,s||i(" "),i("? "),Ga(o,t),t.indentLevel--,s&&a(),s||i(" "),i(": ");const u=19===r.type;u||t.indentLevel++;Ga(r,t),u||t.indentLevel--;s&&c(!0)}(e,t);break;case 20:!function(e,t){const{push:n,helper:o,indent:r,deindent:s,newline:i}=t;n(`_cache[${e.index}] || (`),e.isVNode&&(r(),n(`${o(fc)}(-1),`),i());n(`_cache[${e.index}] = `),Ga(e.value,t),e.isVNode&&(n(","),i(),n(`${o(fc)}(1),`),i(),n(`_cache[${e.index}]`),s());n(")")}(e,t);break;case 21:Ka(e.body,t,!0,!1)}}function qa(e,t){const{content:n,isStatic:o}=e;t.push(o?JSON.stringify(n):n,e)}function Ja(e,t){for(let n=0;n<e.children.length;n++){const o=e.children[n];P(o)?t.push(o):Ga(o,t)}}function Ya(e,t){const{push:n}=t;if(8===e.type)n("["),Ja(e,t),n("]");else if(e.isStatic){n(Mc(e.content)?e.content:JSON.stringify(e.content),e)}else n(`[${e.content}]`,e)}const Za=Ua(/^(if|else|else-if)$/,((e,t,n)=>function(e,t,n,o){if(!("else"===t.name||t.exp&&t.exp.content.trim())){t.exp=Tc("true",!1,t.exp?t.exp.loc:e.loc)}if("if"===t.name){const r=Qa(e,t),s={type:9,loc:e.loc,branches:[r]};if(n.replaceNode(s),o)return o(s,r,!0)}else{const r=n.parent.children;let s=r.indexOf(e);for(;s-- >=-1;){const i=r[s];if(!i||2!==i.type||i.content.trim().length){if(i&&9===i.type){n.removeNode();const r=Qa(e,t);i.branches.push(r);const s=o&&o(i,r,!1);ja(r,n),s&&s(),n.currentNode=null}break}n.removeNode(i)}}}(e,t,n,((e,t,o)=>{const r=n.parent.children;let s=r.indexOf(e),i=0;for(;s-- >=0;){const e=r[s];e&&9===e.type&&(i+=e.branches.length)}return()=>{if(o)e.codegenNode=Xa(t,i,n);else{const o=function(e){for(;;)if(19===e.type){if(19!==e.alternate.type)return e;e=e.alternate}else 20===e.type&&(e=e.value)}(e.codegenNode);o.alternate=Xa(t,i+e.branches.length-1,n)}}}))));function Qa(e,t){const n=3===e.tagType;return{type:10,loc:e.loc,condition:"else"===t.name?void 0:t.exp,children:n&&!Hc(e,"for")?e.children:[e],userKey:Wc(e,"key"),isTemplateIf:n}}function Xa(e,t,n){return e.condition?Oc(e.condition,eu(e,t,n),Ec(n.helper(zl),['""',"true"])):eu(e,t,n)}function eu(e,t,n){const{helper:o}=n,r=kc("key",Tc(`${t}`,!1,Sc,2)),{children:s}=e,i=s[0];if(1!==s.length||1!==i.type){if(1===s.length&&11===i.type){const e=i.codegenNode;return ea(e,r,n),e}{let t=64;return xc(n,o(Ml),wc([r]),s,t+"",void 0,void 0,!0,!1,!1,e.loc)}}{const e=i.codegenNode,t=14===(l=e).type&&l.callee===yc?l.arguments[1].returns:l;return 13===t.type&&na(t,n),ea(t,r,n),e}var l}const tu=Ua("for",((e,t,n)=>{const{helper:o,removeHelper:r}=n;return function(e,t,n,o){if(!t.exp)return;const r=su(t.exp);if(!r)return;const{scopes:s}=n,{source:i,value:l,key:c,index:a}=r,u={type:11,loc:t.loc,source:i,valueAlias:l,keyAlias:c,objectIndexAlias:a,parseResult:r,children:qc(e)?e.children:[e]};n.replaceNode(u),s.vFor++;const p=o&&o(u);return()=>{s.vFor--,p&&p()}}(e,t,n,(t=>{const s=Ec(o(Xl),[t.source]),i=qc(e),l=Hc(e,"memo"),c=Wc(e,"key"),a=c&&(6===c.type?Tc(c.value.content,!0):c.exp),u=c?kc("key",a):null,p=4===t.source.type&&t.source.constType>0,f=p?64:c?128:256;return t.codegenNode=xc(n,o(Ml),void 0,s,f+"",void 0,void 0,!0,!p,!1,e.loc),()=>{let c;const{children:f}=t,d=1!==f.length||1!==f[0].type,h=Jc(e)?e:i&&1===e.children.length&&Jc(e.children[0])?e.children[0]:null;if(h?(c=h.codegenNode,i&&u&&ea(c,u,n)):d?c=xc(n,o(Ml),u?wc([u]):void 0,e.children,"64",void 0,void 0,!0,void 0,!1):(c=f[0].codegenNode,i&&u&&ea(c,u,n),c.isBlock!==!p&&(c.isBlock?(r(jl),r(Zc(n.inSSR,c.isComponent))):r(Yc(n.inSSR,c.isComponent))),c.isBlock=!p,c.isBlock?(o(jl),o(Zc(n.inSSR,c.isComponent))):o(Yc(n.inSSR,c.isComponent))),l){const e=$c(lu(t.parseResult,[Tc("_cached")]));e.body={type:21,body:[Nc(["const _memo = (",l.exp,")"]),Nc(["if (_cached",...a?[" && _cached.key === ",a]:[],` && ${n.helperString(_c)}(_cached, _memo)) return _cached`]),Nc(["const _item = ",c]),Tc("_item.memo = _memo"),Tc("return _item")],loc:Sc},s.arguments.push(e,Tc("_cache"),Tc(String(n.cached++)))}else s.arguments.push($c(lu(t.parseResult),c,!0))}}))}));const nu=/([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/,ou=/,([^,\}\]]*)(?:,([^,\}\]]*))?$/,ru=/^\(|\)$/g;function su(e,t){const n=e.loc,o=e.content,r=o.match(nu);if(!r)return;const[,s,i]=r,l={source:iu(n,i.trim(),o.indexOf(i,s.length)),value:void 0,key:void 0,index:void 0};let c=s.trim().replace(ru,"").trim();const a=s.indexOf(c),u=c.match(ou);if(u){c=c.replace(ou,"").trim();const e=u[1].trim();let t;if(e&&(t=o.indexOf(e,a+c.length),l.key=iu(n,e,t)),u[2]){const r=u[2].trim();r&&(l.index=iu(n,r,o.indexOf(r,l.key?t+e.length:a+c.length)))}}return c&&(l.value=iu(n,c,a)),l}function iu(e,t,n){return Tc(t,!1,jc(e,n,t.length))}function lu({value:e,key:t,index:n},o=[]){return function(e){let t=e.length;for(;t--&&!e[t];);return e.slice(0,t+1).map(((e,t)=>e||Tc("_".repeat(t+1),!1)))}([e,t,n,...o])}const cu=Tc("undefined",!1),au=(e,t)=>{if(1===e.type&&(1===e.tagType||3===e.tagType)){const n=Hc(e,"slot");if(n)return t.scopes.vSlot++,()=>{t.scopes.vSlot--}}},uu=(e,t,n)=>$c(e,t,!1,!0,t.length?t[0].loc:n);function pu(e,t,n=uu){t.helper(mc);const{children:o,loc:r}=e,s=[],i=[];let l=t.scopes.vSlot>0||t.scopes.vFor>0;const c=Hc(e,"slot",!0);if(c){const{arg:e,exp:t}=c;e&&!Rc(e)&&(l=!0),s.push(kc(e||Tc("default",!0),n(t,o,r)))}let a=!1,u=!1;const p=[],f=new Set;for(let m=0;m<o.length;m++){const e=o[m];let r;if(!qc(e)||!(r=Hc(e,"slot",!0))){3!==e.type&&p.push(e);continue}if(c)break;a=!0;const{children:d,loc:h}=e,{arg:g=Tc("default",!0),exp:v}=r;let y;Rc(g)?y=g?g.content:"default":l=!0;const _=n(v,d,h);let b,S,x;if(b=Hc(e,"if"))l=!0,i.push(Oc(b.exp,fu(g,_),cu));else if(S=Hc(e,/^else(-if)?$/,!0)){let e,t=m;for(;t--&&(e=o[t],3===e.type););if(e&&qc(e)&&Hc(e,"if")){o.splice(m,1),m--;let e=i[i.length-1];for(;19===e.alternate.type;)e=e.alternate;e.alternate=S.exp?Oc(S.exp,fu(g,_),cu):fu(g,_)}}else if(x=Hc(e,"for")){l=!0;const e=x.parseResult||su(x.exp);e&&i.push(Ec(t.helper(Xl),[e.source,$c(lu(e),fu(g,_),!0)]))}else{if(y){if(f.has(y))continue;f.add(y),"default"===y&&(u=!0)}s.push(kc(g,_))}}if(!c){const e=(e,t)=>kc("default",n(e,t,r));a?p.length&&p.some((e=>hu(e)))&&(u||s.push(e(void 0,p))):s.push(e(void 0,o))}const d=l?2:du(e.children)?3:1;let h=wc(s.concat(kc("_",Tc(d+"",!1))),r);return i.length&&(h=Ec(t.helper(tc),[h,Cc(i)])),{slots:h,hasDynamicSlots:l}}function fu(e,t){return wc([kc("name",e),kc("fn",t)])}function du(e){for(let t=0;t<e.length;t++){const n=e[t];switch(n.type){case 1:if(2===n.tagType||du(n.children))return!0;break;case 9:if(du(n.branches))return!0;break;case 10:case 11:if(du(n.children))return!0}}return!1}function hu(e){return 2!==e.type&&12!==e.type||(2===e.type?!!e.content.trim():hu(e.content))}const mu=new WeakMap,gu=(e,t)=>function(){if(1!==(e=t.currentNode).type||0!==e.tagType&&1!==e.tagType)return;const{tag:n,props:o}=e,r=1===e.tagType;let s=r?function(e,t,n=!1){let{tag:o}=e;const r=bu(o),s=Wc(e,"is");if(s)if(r){const e=6===s.type?s.value&&Tc(s.value.content,!0):s.exp;if(e)return Ec(t.helper(Jl),[e])}else 6===s.type&&s.value.content.startsWith("vue:")&&(o=s.value.content.slice(4));const i=!r&&Hc(e,"is");if(i&&i.exp)return Ec(t.helper(Jl),[i.exp]);const l=Pc(o)||t.isBuiltInComponent(o);if(l)return n||t.helper(l),l;return t.helper(ql),t.components.add(o),ta(o,"component")}(e,t):`"${n}"`;const i=M(s)&&s.callee===Jl;let l,c,a,u,p,f,d=0,h=i||s===Vl||s===Il||!r&&("svg"===n||"foreignObject"===n);if(o.length>0){const n=vu(e,t,void 0,r,i);l=n.props,d=n.patchFlag,p=n.dynamicPropNames;const o=n.directives;f=o&&o.length?Cc(o.map((e=>function(e,t){const n=[],o=mu.get(e);o?n.push(t.helperString(o)):(t.helper(Yl),t.directives.add(e.name),n.push(ta(e.name,"directive")));const{loc:r}=e;e.exp&&n.push(e.exp);e.arg&&(e.exp||n.push("void 0"),n.push(e.arg));if(Object.keys(e.modifiers).length){e.arg||(e.exp||n.push("void 0"),n.push("void 0"));const t=Tc("true",!1,r);n.push(wc(e.modifiers.map((e=>kc(e,t))),r))}return Cc(n,e.loc)}(e,t)))):void 0,n.shouldUseBlock&&(h=!0)}if(e.children.length>0){s===Bl&&(h=!0,d|=1024);if(r&&s!==Vl&&s!==Bl){const{slots:n,hasDynamicSlots:o}=pu(e,t);c=n,o&&(d|=1024)}else if(1===e.children.length&&s!==Vl){const n=e.children[0],o=n.type,r=5===o||8===o;r&&0===Fa(n,t)&&(d|=1),c=r||2===o?n:e.children}else c=e.children}0!==d&&(a=String(d),p&&p.length&&(u=function(e){let t="[";for(let n=0,o=e.length;n<o;n++)t+=JSON.stringify(e[n]),n<o-1&&(t+=", ");return t+"]"}(p))),e.codegenNode=xc(t,s,l,c,a,u,f,!!h,!1,r,e.loc)};function vu(e,t,n=e.props,o,r,s=!1){const{tag:i,loc:l,children:c}=e;let a=[];const u=[],p=[],f=c.length>0;let d=!1,h=0,m=!1,g=!1,v=!1,y=!1,_=!1,b=!1;const S=[],C=({key:e,value:n})=>{if(Rc(e)){const s=e.content,i=x(s);if(!i||o&&!r||"onclick"===s.toLowerCase()||"onUpdate:modelValue"===s||U(s)||(y=!0),i&&U(s)&&(b=!0),20===n.type||(4===n.type||8===n.type)&&Fa(n,t)>0)return;"ref"===s?m=!0:"class"===s?g=!0:"style"===s?v=!0:"key"===s||S.includes(s)||S.push(s),!o||"class"!==s&&"style"!==s||S.includes(s)||S.push(s)}else _=!0};for(let x=0;x<n.length;x++){const o=n[x];if(6===o.type){const{loc:e,name:n,value:r}=o;let s=!0;if("ref"===n&&(m=!0,t.scopes.vFor>0&&a.push(kc(Tc("ref_for",!0),Tc("true")))),"is"===n&&(bu(i)||r&&r.content.startsWith("vue:")))continue;a.push(kc(Tc(n,!0,jc(e,0,n.length)),Tc(r?r.content:"",s,r?r.loc:e)))}else{const{name:n,arg:r,exp:c,loc:h}=o,m="bind"===n,g="on"===n;if("slot"===n)continue;if("once"===n||"memo"===n)continue;if("is"===n||m&&zc(r,"is")&&bu(i))continue;if(g&&s)continue;if((m&&zc(r,"key")||g&&f&&zc(r,"vue:before-update"))&&(d=!0),m&&zc(r,"ref")&&t.scopes.vFor>0&&a.push(kc(Tc("ref_for",!0),Tc("true"))),!r&&(m||g)){_=!0,c&&(a.length&&(u.push(wc(yu(a),l)),a=[]),u.push(m?c:{type:14,loc:h,callee:t.helper(cc),arguments:[c]}));continue}const v=t.directiveTransforms[n];if(v){const{props:n,needRuntime:r}=v(o,e,t);!s&&n.forEach(C),a.push(...n),r&&(p.push(o),A(r)&&mu.set(o,r))}else D(n)||(p.push(o),f&&(d=!0))}}let w;if(u.length?(a.length&&u.push(wc(yu(a),l)),w=u.length>1?Ec(t.helper(oc),u,l):u[0]):a.length&&(w=wc(yu(a),l)),_?h|=16:(g&&!o&&(h|=2),v&&!o&&(h|=4),S.length&&(h|=8),y&&(h|=32)),d||0!==h&&32!==h||!(m||b||p.length>0)||(h|=512),!t.inSSR&&w)switch(w.type){case 15:let e=-1,n=-1,o=!1;for(let t=0;t<w.properties.length;t++){const r=w.properties[t].key;Rc(r)?"class"===r.content?e=t:"style"===r.content&&(n=t):r.isHandlerKey||(o=!0)}const r=w.properties[e],s=w.properties[n];o?w=Ec(t.helper(ic),[w]):(r&&!Rc(r.value)&&(r.value=Ec(t.helper(rc),[r.value])),s&&(v||4===s.value.type&&"["===s.value.content.trim()[0]||17===s.value.type)&&(s.value=Ec(t.helper(sc),[s.value])));break;case 14:break;default:w=Ec(t.helper(ic),[Ec(t.helper(lc),[w])])}return{props:w,directives:p,patchFlag:h,dynamicPropNames:S,shouldUseBlock:d}}function yu(e){const t=new Map,n=[];for(let o=0;o<e.length;o++){const r=e[o];if(8===r.key.type||!r.key.isStatic){n.push(r);continue}const s=r.key.content,i=t.get(s);i?("style"===s||"class"===s||x(s))&&_u(i,r):(t.set(s,r),n.push(r))}return n}function _u(e,t){17===e.value.type?e.value.elements.push(t.value):e.value=Cc([e.value,t.value],e.loc)}function bu(e){return"component"===e||"Component"===e}const Su=(e,t)=>{if(Jc(e)){const{children:n,loc:o}=e,{slotName:r,slotProps:s}=function(e,t){let n,o='"default"';const r=[];for(let s=0;s<e.props.length;s++){const t=e.props[s];6===t.type?t.value&&("name"===t.name?o=JSON.stringify(t.value.content):(t.name=z(t.name),r.push(t))):"bind"===t.name&&zc(t.arg,"name")?t.exp&&(o=t.exp):("bind"===t.name&&t.arg&&Rc(t.arg)&&(t.arg.content=z(t.arg.content)),r.push(t))}if(r.length>0){const{props:o,directives:s}=vu(e,t,r,!1,!1);n=o}return{slotName:o,slotProps:n}}(e,t),i=[t.prefixIdentifiers?"_ctx.$slots":"$slots",r,"{}","undefined","true"];let l=2;s&&(i[2]=s,l=3),n.length&&(i[3]=$c([],n,!1,!1,o),l=4),t.scopeId&&!t.slotted&&(l=5),i.splice(l),e.codegenNode=Ec(t.helper(ec),i,o)}};const xu=/^\s*([\w$_]+|(async\s*)?\([^)]*?\))\s*=>|^\s*(async\s+)?function(?:\s+[\w$]+)?\s*\(/,Cu=(e,t,n,o)=>{const{loc:r,modifiers:s,arg:i}=e;let l;if(4===i.type)if(i.isStatic){let e=i.content;e.startsWith("vue:")&&(e=`vnode-${e.slice(4)}`),l=Tc(J(z(e)),!0,i.loc)}else l=Nc([`${n.helperString(pc)}(`,i,")"]);else l=i,l.children.unshift(`${n.helperString(pc)}(`),l.children.push(")");let c=e.exp;c&&!c.content.trim()&&(c=void 0);let a=n.cacheHandlers&&!c&&!n.inVOnce;if(c){const e=Lc(c.content),t=!(e||xu.test(c.content)),n=c.content.includes(";");(t||a&&e)&&(c=Nc([`${t?"$event":"(...args)"} => ${n?"{":"("}`,c,n?"}":")"]))}let u={props:[kc(l,c||Tc("() => {}",!1,r))]};return o&&(u=o(u)),a&&(u.props[0].value=n.cache(u.props[0].value)),u.props.forEach((e=>e.key.isHandlerKey=!0)),u},wu=(e,t,n)=>{const{exp:o,modifiers:r,loc:s}=e,i=e.arg;return 4!==i.type?(i.children.unshift("("),i.children.push(') || ""')):i.isStatic||(i.content=`${i.content} || ""`),r.includes("camel")&&(4===i.type?i.content=i.isStatic?z(i.content):`${n.helperString(ac)}(${i.content})`:(i.children.unshift(`${n.helperString(ac)}(`),i.children.push(")"))),n.inSSR||(r.includes("prop")&&ku(i,"."),r.includes("attr")&&ku(i,"^")),!o||4===o.type&&!o.content.trim()?{props:[kc(i,Tc("",!0,s))]}:{props:[kc(i,o)]}},ku=(e,t)=>{4===e.type?e.content=e.isStatic?t+e.content:`\`${t}\${${e.content}}\``:(e.children.unshift(`'${t}' + (`),e.children.push(")"))},Tu=(e,t)=>{if(0===e.type||1===e.type||11===e.type||10===e.type)return()=>{const n=e.children;let o,r=!1;for(let e=0;e<n.length;e++){const t=n[e];if(Kc(t)){r=!0;for(let r=e+1;r<n.length;r++){const s=n[r];if(!Kc(s)){o=void 0;break}o||(o=n[e]=Nc([t],t.loc)),o.children.push(" + ",s),n.splice(r,1),r--}}}if(r&&(1!==n.length||0!==e.type&&(1!==e.type||0!==e.tagType||e.props.find((e=>7===e.type&&!t.directiveTransforms[e.name])))))for(let e=0;e<n.length;e++){const o=n[e];if(Kc(o)||8===o.type){const r=[];2===o.type&&" "===o.content||r.push(o),t.ssr||0!==Fa(o,t)||r.push("1"),n[e]={type:12,content:o,loc:o.loc,codegenNode:Ec(t.helper(Kl),r)}}}}},Nu=new WeakSet,Eu=(e,t)=>{if(1===e.type&&Hc(e,"once",!0)){if(Nu.has(e)||t.inVOnce)return;return Nu.add(e),t.inVOnce=!0,t.helper(fc),()=>{t.inVOnce=!1;const e=t.currentNode;e.codegenNode&&(e.codegenNode=t.cache(e.codegenNode,!0))}}},$u=(e,t,n)=>{const{exp:o,arg:r}=e;if(!o)return Ou();const s=o.loc.source,i=4===o.type?o.content:s;if(!i.trim()||!Lc(i))return Ou();const l=r||Tc("modelValue",!0),c=r?Rc(r)?`onUpdate:${r.content}`:Nc(['"onUpdate:" + ',r]):"onUpdate:modelValue";let a;a=Nc([`${n.isTS?"($event: any)":"$event"} => ((`,o,") = $event)"]);const u=[kc(l,e.exp),kc(c,a)];if(e.modifiers.length&&1===t.tagType){const t=e.modifiers.map((e=>(Mc(e)?e:JSON.stringify(e))+": true")).join(", "),n=r?Rc(r)?`${r.content}Modifiers`:Nc([r,' + "Modifiers"']):"modelModifiers";u.push(kc(n,Tc(`{ ${t} }`,!1,e.loc,2)))}return Ou(u)};function Ou(e=[]){return{props:e}}const Ru=new WeakSet,Fu=(e,t)=>{if(1===e.type){const n=Hc(e,"memo");if(!n||Ru.has(e))return;return Ru.add(e),()=>{const o=e.codegenNode||t.currentNode.codegenNode;o&&13===o.type&&(1!==e.tagType&&na(o,t),e.codegenNode=Ec(t.helper(yc),[n.exp,$c(void 0,o),"_cache",String(t.cached++)]))}}};function Pu(e,t={}){const n=t.onError||Fl,o="module"===t.mode;!0===t.prefixIdentifiers?n(Al(46)):o&&n(Al(47));t.cacheHandlers&&n(Al(48)),t.scopeId&&!o&&n(Al(49));const r=P(e)?ia(e,t):e,[s,i]=[[Eu,Za,Fu,tu,Su,gu,au,Tu],{on:Cu,bind:wu,model:$u}];return La(r,w({},t,{prefixIdentifiers:false,nodeTransforms:[...s,...t.nodeTransforms||[]],directiveTransforms:w({},i,t.directiveTransforms||{})})),Ha(r,w({},t,{prefixIdentifiers:false}))}const Au=Symbol(""),Mu=Symbol(""),Vu=Symbol(""),Iu=Symbol(""),Bu=Symbol(""),Lu=Symbol(""),ju=Symbol(""),Uu=Symbol(""),Du=Symbol(""),Hu=Symbol("");var Wu;let zu;Wu={[Au]:"vModelRadio",[Mu]:"vModelCheckbox",[Vu]:"vModelText",[Iu]:"vModelSelect",[Bu]:"vModelDynamic",[Lu]:"withModifiers",[ju]:"withKeys",[Uu]:"vShow",[Du]:"Transition",[Hu]:"TransitionGroup"},Object.getOwnPropertySymbols(Wu).forEach((e=>{bc[e]=Wu[e]}));const Ku=e("style,iframe,script,noscript",!0),Gu={isVoidTag:f,isNativeTag:e=>u(e)||p(e),isPreTag:e=>"pre"===e,decodeEntities:function(e,t=!1){return zu||(zu=document.createElement("div")),t?(zu.innerHTML=`<div foo="${e.replace(/"/g,""")}">`,zu.children[0].getAttribute("foo")):(zu.innerHTML=e,zu.textContent)},isBuiltInComponent:e=>Fc(e,"Transition")?Du:Fc(e,"TransitionGroup")?Hu:void 0,getNamespace(e,t){let n=t?t.ns:0;if(t&&2===n)if("annotation-xml"===t.tag){if("svg"===e)return 1;t.props.some((e=>6===e.type&&"encoding"===e.name&&null!=e.value&&("text/html"===e.value.content||"application/xhtml+xml"===e.value.content)))&&(n=0)}else/^m(?:[ions]|text)$/.test(t.tag)&&"mglyph"!==e&&"malignmark"!==e&&(n=0);else t&&1===n&&("foreignObject"!==t.tag&&"desc"!==t.tag&&"title"!==t.tag||(n=0));if(0===n){if("svg"===e)return 1;if("math"===e)return 2}return n},getTextMode({tag:e,ns:t}){if(0===t){if("textarea"===e||"title"===e)return 1;if(Ku(e))return 2}return 0}},qu=(e,t)=>{const n=l(e);return Tc(JSON.stringify(n),!1,t,3)};const Ju=e("passive,once,capture"),Yu=e("stop,prevent,self,ctrl,shift,alt,meta,exact,middle"),Zu=e("left,right"),Qu=e("onkeyup,onkeydown,onkeypress",!0),Xu=(e,t)=>Rc(e)&&"onclick"===e.content.toLowerCase()?Tc(t,!0):4!==e.type?Nc(["(",e,`) === "onClick" ? "${t}" : (`,e,")"]):e,ep=(e,t)=>{1!==e.type||0!==e.tagType||"script"!==e.tag&&"style"!==e.tag||t.removeNode()},tp=[e=>{1===e.type&&e.props.forEach(((t,n)=>{6===t.type&&"style"===t.name&&t.value&&(e.props[n]={type:7,name:"bind",arg:Tc("style",!0,t.loc),exp:qu(t.value.content,t.loc),modifiers:[],loc:t.loc})}))}],np={cloak:()=>({props:[]}),html:(e,t,n)=>{const{exp:o,loc:r}=e;return t.children.length&&(t.children.length=0),{props:[kc(Tc("innerHTML",!0,r),o||Tc("",!0))]}},text:(e,t,n)=>{const{exp:o,loc:r}=e;return t.children.length&&(t.children.length=0),{props:[kc(Tc("textContent",!0),o?Fa(o,n)>0?o:Ec(n.helperString(nc),[o],r):Tc("",!0))]}},model:(e,t,n)=>{const o=$u(e,t,n);if(!o.props.length||1===t.tagType)return o;const{tag:r}=t,s=n.isCustomElement(r);if("input"===r||"textarea"===r||"select"===r||s){let e=Vu,i=!1;if("input"===r||s){const n=Wc(t,"type");if(n){if(7===n.type)e=Bu;else if(n.value)switch(n.value.content){case"radio":e=Au;break;case"checkbox":e=Mu;break;case"file":i=!0}}else(function(e){return e.props.some((e=>!(7!==e.type||"bind"!==e.name||e.arg&&4===e.arg.type&&e.arg.isStatic)))})(t)&&(e=Bu)}else"select"===r&&(e=Iu);i||(o.needRuntime=n.helper(e))}return o.props=o.props.filter((e=>!(4===e.key.type&&"modelValue"===e.key.content))),o},on:(e,t,n)=>Cu(e,0,n,(t=>{const{modifiers:o}=e;if(!o.length)return t;let{key:r,value:s}=t.props[0];const{keyModifiers:i,nonKeyModifiers:l,eventOptionModifiers:c}=((e,t,n,o)=>{const r=[],s=[],i=[];for(let l=0;l<t.length;l++){const n=t[l];Ju(n)?i.push(n):Zu(n)?Rc(e)?Qu(e.content)?r.push(n):s.push(n):(r.push(n),s.push(n)):Yu(n)?s.push(n):r.push(n)}return{keyModifiers:r,nonKeyModifiers:s,eventOptionModifiers:i}})(r,o);if(l.includes("right")&&(r=Xu(r,"onContextmenu")),l.includes("middle")&&(r=Xu(r,"onMouseup")),l.length&&(s=Ec(n.helper(Lu),[s,JSON.stringify(l)])),!i.length||Rc(r)&&!Qu(r.content)||(s=Ec(n.helper(ju),[s,JSON.stringify(i)])),c.length){const e=c.map(q).join("");r=Rc(r)?Tc(`${r.content}${e}`,!0):Nc(["(",r,`) + "${e}"`])}return{props:[kc(r,s)]}})),show:(e,t,n)=>({props:[],needRuntime:n.helper(Uu)})};const op=Object.create(null);function rp(e,t){if(!P(e)){if(!e.nodeType)return _;e=e.innerHTML}const n=e,o=op[n];if(o)return o;if("#"===e[0]){const t=document.querySelector(e);e=t?t.innerHTML:""}const{code:r}=function(e,t={}){return Pu(e,w({},Gu,t,{nodeTransforms:[ep,...tp,...t.nodeTransforms||[]],directiveTransforms:w({},np,t.directiveTransforms||{}),transformHoist:null}))}(e,w({hoistStatic:!0,onError:void 0,onWarn:_},t)),s=new Function("Vue",r)(Rl);return s._rc=!0,op[n]=s}Fs(rp);export{io as BaseTransition,Kr as Comment,ne as EffectScope,Wr as Fragment,_o as KeepAlive,ge as ReactiveEffect,Gr as Static,Un as Suspense,Hr as Teleport,zr as Text,Fi as Transition,Yi as TransitionGroup,Ni as VueElement,Zt as callWithAsyncErrorHandling,Yt as callWithErrorHandling,z as camelize,q as capitalize,fs as cloneVNode,ii as compatUtils,rp as compile,js as computed,Nl as createApp,ns as createBlock,ms as createCommentVNode,ts as createElementBlock,as as createElementVNode,Mr as createHydrationRenderer,Js as createPropsRestProxy,Ar as createRenderer,El as createSSRApp,qo as createSlots,hs as createStaticVNode,ds as createTextVNode,us as createVNode,Ut as customRef,go as defineAsyncComponent,ho as defineComponent,wi as defineCustomElement,Ds as defineEmits,Hs as defineExpose,Us as defineProps,ki as defineSSRCustomElement,xn as devtools,ye as effect,oe as effectScope,ws as getCurrentInstance,se as getCurrentScope,fo as getTransitionRawChildren,ps as guardReactiveProps,Zs as h,Qt as handleError,Tl as hydrate,ei as initCustomFormatter,Ol as initDirectivesForSSR,qn as inject,ni as isMemoSame,wt as isProxy,St as isReactive,xt as isReadonly,Rt as isRef,Ps as isRuntimeOnly,Ct as isShallow,os as isVNode,Tt as markRaw,qs as mergeDefaults,_s as mergeProps,dn as nextTick,c as normalizeClass,a as normalizeProps,r as normalizeStyle,So as onActivated,$o as onBeforeMount,Po as onBeforeUnmount,Ro as onBeforeUpdate,xo as onDeactivated,Bo as onErrorCaptured,Oo as onMounted,Io as onRenderTracked,Vo as onRenderTriggered,ie as onScopeDispose,Mo as onServerPrefetch,Ao as onUnmounted,Fo as onUpdated,Yr as openBlock,Fn as popScopeId,Gn as provide,Lt as proxyRefs,Rn as pushScopeId,vn as queuePostFlushCb,gt as reactive,yt as readonly,Ft as ref,Fs as registerRuntimeCompiler,kl as render,Go as renderList,Jo as renderSlot,Uo as resolveComponent,Wo as resolveDirective,Ho as resolveDynamicComponent,si as resolveFilter,co as resolveTransitionHooks,Xr as setBlockTracking,wn as setDevtoolsHook,po as setTransitionHooks,vt as shallowReactive,_t as shallowReadonly,Pt as shallowRef,Qs as ssrContextKey,ri as ssrUtils,_e as stop,m as toDisplayString,J as toHandlerKey,Zo as toHandlers,kt as toRaw,Wt as toRef,Dt as toRefs,ss as transformVNodeArgs,Vt as triggerRef,It as unref,Ks as useAttrs,Ei as useCssModule,$i as useCssVars,Xs as useSSRContext,zs as useSlots,ro as useTransitionState,rl as vModelCheckbox,pl as vModelDynamic,il as vModelRadio,ll as vModelSelect,ol as vModelText,yl as vShow,oi as version,Gt as warn,Xn as watch,Jn as watchEffect,Yn as watchPostEffect,Zn as watchSyncEffect,Ys as withAsyncContext,An as withCtx,Ws as withDefaults,Lo as withDirectives,vl as withKeys,ti as withMemo,ml as withModifiers,Pn as withScopeId}; ================================================ FILE: src/electionguard_gui/web/services/authorization-service.js ================================================ export default { _userId: undefined, _isAdmin: undefined, async getUserId() { if (!this._userId) { this._userId = await eel.get_user_id()(); } return this._userId; }, async setUserId(id) { await eel.set_user_id(id)(); this._userId = id; }, async isAdmin() { if (!this._isAdmin) { this._isAdmin = await eel.is_admin()(); } return this._isAdmin; }, }; ================================================ FILE: src/electionguard_gui/web/services/router-service.js ================================================ // shared components import Home from "../components/shared/home-component.js"; import NotFound from "../components/shared/not-found-component.js"; import Login from "../components/shared/login-component.js"; // admin components import AdminHome from "../components/admin/admin-home-component.js"; import CreateElection from "../components/admin/create-election-component.js"; import CreateKeyCeremony from "../components/admin/create-key-ceremony-component.js"; import ViewKeyCeremonyAdmin from "../components/admin/view-key-ceremony-component.js"; import ViewElectionAdmin from "../components/admin/view-election-component.js"; import ExportEncryptionPackage from "../components/admin/export-encryption-package-component.js"; import ExportElectionRecord from "../components/admin/export-election-record-component.js"; import UploadBallots from "../components/admin/upload-ballots-component.js"; import CreateDecryption from "../components/admin/create-decryption-component.js"; import ViewDecryptionAdmin from "../components/admin/view-decryption-admin-component.js"; import ViewTally from "../components/admin/view-tally-component.js"; import ViewSpoiledBallot from "../components/admin/view-spoiled-ballot-component.js"; // guardian components import GuardianHome from "../components/guardian/guardian-home-component.js"; import ViewKeyCeremonyGuardian from "../components/guardian/view-key-ceremony-component.js"; import ViewDecryptionGuardian from "../components/guardian/view-decryption-guardian-component.js"; export default { getUrl(route, params) { if (!route) throw new Error("Invalid route specified"); return "#" + route.url + "?" + new URLSearchParams(params); }, goTo(route, params) { const urlWithParams = this.getUrl(route, params); window.location.href = urlWithParams; }, getRouteByUrl(url) { return Object.values(this.routes).filter((r) => r.url === url)[0]; }, getRoute(path) { const cleanPath = path.split("?")[0].slice(1) || "/"; const foundRoute = this.getRouteByUrl(cleanPath); console.log("getRoute", cleanPath, foundRoute); return foundRoute || this.routes.notFound; }, routes: { // shared pages root: { url: "/", secured: true, component: Home }, notFound: { url: "/not-found", secured: false, component: NotFound }, login: { url: "/login", secured: false, component: Login }, // admin pages adminHome: { url: "/admin/home", secured: true, component: AdminHome }, createElection: { url: "/admin/create-election", secured: true, component: CreateElection, }, viewElectionAdmin: { url: "/admin/view-election", secured: true, component: ViewElectionAdmin, }, exportEncryptionPackage: { url: "/admin/export-encryption-package", secured: true, component: ExportEncryptionPackage, }, exportElectionRecord: { url: "/admin/export-election-record", secured: true, component: ExportElectionRecord, }, createKeyCeremony: { url: "/admin/create-key-ceremony", secured: true, component: CreateKeyCeremony, }, viewKeyCeremonyAdminPage: { url: "/admin/view-key-ceremony", secured: true, component: ViewKeyCeremonyAdmin, }, uploadBallots: { url: "/admin/upload-ballots", secured: true, component: UploadBallots, }, createDecryption: { url: "/admin/create-decryption", secured: true, component: CreateDecryption, }, viewDecryptionAdmin: { url: "/admin/view-decryption", secured: true, component: ViewDecryptionAdmin, }, viewTally: { url: "/admin/view-tally", secured: true, component: ViewTally, }, viewSpoiledBallot: { url: "/admin/view-spoiled-ballot", secured: true, component: ViewSpoiledBallot, }, // guardian pages guardianHome: { url: "/guardian/home", secured: true, component: GuardianHome, }, viewKeyCeremonyGuardianPage: { url: "/guardian/view-key-ceremony", secured: true, component: ViewKeyCeremonyGuardian, }, viewDecryptionGuardian: { url: "/guardian/view-decryption", secured: true, component: ViewDecryptionGuardian, }, }, getElectionUrl(electionId) { return this.getUrl(this.routes.viewElectionAdmin, { electionId: electionId, }); }, }; ================================================ FILE: src/electionguard_gui/web/site.webmanifest ================================================ { "name": "ElectionGuard", "short_name": "ElectionGuard", "icons": [ { "src": "/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" }, { "src": "/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" } ], "theme_color": "#009688", "background_color": "#ffffff", "display": "standalone" } ================================================ FILE: src/electionguard_tools/__init__.py ================================================ import importlib.metadata # <AUTOGEN_INIT> from electionguard_tools import factories from electionguard_tools import helpers from electionguard_tools import scripts from electionguard_tools import strategies from electionguard_tools.factories import ( AllPrivateElectionData, AllPublicElectionData, BallotFactory, ElectionFactory, NUMBER_OF_GUARDIANS, QUORUM, ballot_factory, election_factory, get_contest_description_well_formed, get_selection_description_well_formed, get_selection_poorly_formed, get_selection_well_formed, ) from electionguard_tools.helpers import ( CIPHERTEXT_BALLOT_PREFIX, COEFFICIENTS_FILE_NAME, CONSTANTS_FILE_NAME, CONTEXT_FILE_NAME, DEVICES_DIR, DEVICE_PREFIX, ELECTION_RECORD_DIR, ENCRYPTED_TALLY_FILE_NAME, ElectionBuilder, GUARDIANS_DIR, GUARDIAN_PREFIX, KeyCeremonyOrchestrator, MANIFEST_FILE_NAME, PLAINTEXT_BALLOT_PREFIX, PRIVATE_DATA_DIR, PRIVATE_GUARDIAN_PREFIX, SPOILED_BALLOTS_DIR, SPOILED_BALLOT_PREFIX, SUBMITTED_BALLOTS_DIR, SUBMITTED_BALLOT_PREFIX, TALLY_FILE_NAME, TallyCeremonyOrchestrator, accumulate_plaintext_ballots, election_builder, export, export_private_data, export_record, key_ceremony_orchestrator, tally_accumulate, tally_ceremony_orchestrator, ) from electionguard_tools.scripts import ( DEFAULT_NUMBER_OF_BALLOTS, DEFAULT_SAMPLE_MANIFEST, DEFAULT_SPEC_VERSION, DEFAULT_SPOIL_RATE, DEFAULT_USE_ALL_GUARDIANS, DEFAULT_USE_PRIVATE_DATA, ElectionSampleDataGenerator, sample_generator, ) from electionguard_tools.strategies import ( CiphertextElectionsTupleType, ElectionsAndBallotsTupleType, annotated_emails, annotated_strings, ballot_styles, candidate_contest_descriptions, candidates, ciphertext_elections, contact_infos, contest_descriptions, contest_descriptions_room_for_overvoting, election, election_descriptions, election_types, elections_and_ballots, elements_mod_p, elements_mod_p_no_zero, elements_mod_q, elements_mod_q_no_zero, elgamal, elgamal_keypairs, geopolitical_units, group, human_names, internationalized_human_names, internationalized_texts, language_human_names, languages, party_lists, plaintext_voted_ballot, plaintext_voted_ballots, referendum_contest_descriptions, reporting_unit_types, two_letter_codes, ) __all__ = [ "AllPrivateElectionData", "AllPublicElectionData", "BallotFactory", "CIPHERTEXT_BALLOT_PREFIX", "COEFFICIENTS_FILE_NAME", "CONSTANTS_FILE_NAME", "CONTEXT_FILE_NAME", "CiphertextElectionsTupleType", "DEFAULT_NUMBER_OF_BALLOTS", "DEFAULT_SAMPLE_MANIFEST", "DEFAULT_SPEC_VERSION", "DEFAULT_SPOIL_RATE", "DEFAULT_USE_ALL_GUARDIANS", "DEFAULT_USE_PRIVATE_DATA", "DEVICES_DIR", "DEVICE_PREFIX", "ELECTION_RECORD_DIR", "ENCRYPTED_TALLY_FILE_NAME", "ElectionBuilder", "ElectionFactory", "ElectionSampleDataGenerator", "ElectionsAndBallotsTupleType", "GUARDIANS_DIR", "GUARDIAN_PREFIX", "KeyCeremonyOrchestrator", "MANIFEST_FILE_NAME", "NUMBER_OF_GUARDIANS", "PLAINTEXT_BALLOT_PREFIX", "PRIVATE_DATA_DIR", "PRIVATE_GUARDIAN_PREFIX", "QUORUM", "SPOILED_BALLOTS_DIR", "SPOILED_BALLOT_PREFIX", "SUBMITTED_BALLOTS_DIR", "SUBMITTED_BALLOT_PREFIX", "TALLY_FILE_NAME", "TallyCeremonyOrchestrator", "accumulate_plaintext_ballots", "annotated_emails", "annotated_strings", "ballot_factory", "ballot_styles", "candidate_contest_descriptions", "candidates", "ciphertext_elections", "contact_infos", "contest_descriptions", "contest_descriptions_room_for_overvoting", "election", "election_builder", "election_descriptions", "election_factory", "election_types", "elections_and_ballots", "elements_mod_p", "elements_mod_p_no_zero", "elements_mod_q", "elements_mod_q_no_zero", "elgamal", "elgamal_keypairs", "export", "export_private_data", "export_record", "factories", "geopolitical_units", "get_contest_description_well_formed", "get_selection_description_well_formed", "get_selection_poorly_formed", "get_selection_well_formed", "group", "helpers", "human_names", "internationalized_human_names", "internationalized_texts", "key_ceremony_orchestrator", "language_human_names", "languages", "party_lists", "plaintext_voted_ballot", "plaintext_voted_ballots", "referendum_contest_descriptions", "reporting_unit_types", "sample_generator", "scripts", "strategies", "tally_accumulate", "tally_ceremony_orchestrator", "two_letter_codes", ] # </AUTOGEN_INIT> # single source version from pyproject.toml try: __version__ = importlib.metadata.version(__package__.split("_", maxsplit=1)[0]) except importlib.metadata.PackageNotFoundError: __version__ = "0.0.0" ================================================ FILE: src/electionguard_tools/factories/__init__.py ================================================ from electionguard_tools.factories import ballot_factory from electionguard_tools.factories import election_factory from electionguard_tools.factories.ballot_factory import ( BallotFactory, get_selection_poorly_formed, get_selection_well_formed, ) from electionguard_tools.factories.election_factory import ( AllPrivateElectionData, AllPublicElectionData, ElectionFactory, NUMBER_OF_GUARDIANS, QUORUM, get_contest_description_well_formed, get_selection_description_well_formed, ) __all__ = [ "AllPrivateElectionData", "AllPublicElectionData", "BallotFactory", "ElectionFactory", "NUMBER_OF_GUARDIANS", "QUORUM", "ballot_factory", "election_factory", "get_contest_description_well_formed", "get_selection_description_well_formed", "get_selection_poorly_formed", "get_selection_well_formed", ] ================================================ FILE: src/electionguard_tools/factories/ballot_factory.py ================================================ from typing import Any, TypeVar, Callable, List, Tuple, Optional import os from random import Random, randint import uuid from hypothesis.strategies import ( composite, booleans, integers, text, uuids, SearchStrategy, ) from electionguard.ballot import ( PlaintextBallot, PlaintextBallotContest, PlaintextBallotSelection, ) from electionguard.encrypt import selection_from from electionguard.manifest import ( ContestDescription, SelectionDescription, InternalManifest, ) from electionguard.serialize import from_file, from_list_in_file _T = TypeVar("_T") _DrawType = Callable[[SearchStrategy[_T]], _T] _data = os.path.realpath(os.path.join(__file__, "../../../../data")) class BallotFactory: """Factory to create ballots""" simple_ballot_filename = "ballot_in_simple.json" simple_ballots_filename = "plaintext_ballots_simple.json" @staticmethod def get_random_selection_from( description: SelectionDescription, random_source: Random, is_placeholder: bool = False, ) -> PlaintextBallotSelection: selected = bool(random_source.randint(0, 1)) return selection_from(description, is_placeholder, selected) @staticmethod def get_random_contest_from( description: ContestDescription, random: Random, suppress_validity_check: bool = False, allow_null_votes: bool = True, allow_under_votes: bool = True, ) -> PlaintextBallotContest: """ Get a randomly filled contest for the given description that may be undervoted and may include explicitly false votes. Since this is only used for testing, the random number generator (`random`) must be provided to make this function deterministic. """ if not suppress_validity_check: assert description.is_valid(), "the contest description must be valid" shuffled_selections = description.ballot_selections[:] random.shuffle(shuffled_selections) if allow_null_votes and not allow_under_votes: cut_point = random.choice([0, description.number_elected]) else: min_votes = description.number_elected if allow_under_votes: min_votes = 1 if allow_null_votes: min_votes = 0 cut_point = random.randint(min_votes, description.number_elected) selections = [ selection_from(selection_description, is_affirmative=True) for selection_description in shuffled_selections[0:cut_point] ] for selection_description in shuffled_selections[cut_point:]: # Possibly append the false selections as well, indicating some choices # may be explicitly false if bool(random.randint(0, 1)) == 1: selections.append( selection_from(selection_description, is_affirmative=False) ) random.shuffle(selections) return PlaintextBallotContest(description.object_id, selections) def get_fake_ballot( self, internal_manifest: InternalManifest, ballot_id: Optional[str] = None, allow_null_votes: bool = False, ) -> PlaintextBallot: """ Get a single Fake Ballot object that is manually constructed with default vaules """ if ballot_id is None: ballot_id = "some-unique-ballot-id-123" contests: List[PlaintextBallotContest] = [] for contest in internal_manifest.get_contests_for( internal_manifest.ballot_styles[0].object_id ): contests.append( self.get_random_contest_from( contest, Random(), allow_null_votes=allow_null_votes ) ) fake_ballot = PlaintextBallot( ballot_id, internal_manifest.ballot_styles[0].object_id, contests ) return fake_ballot def generate_fake_plaintext_ballots_for_election( self, internal_manifest: InternalManifest, number_of_ballots: int, ballot_style_id: Optional[str] = None, allow_null_votes: bool = False, allow_under_votes: bool = True, ) -> List[PlaintextBallot]: ballots: List[PlaintextBallot] = [] for _i in range(number_of_ballots): if ballot_style_id is not None: ballot_style = internal_manifest.get_ballot_style(ballot_style_id) else: style_index = randint(0, len(internal_manifest.ballot_styles) - 1) ballot_style = internal_manifest.ballot_styles[style_index] ballot_id = f"ballot-{uuid.uuid1()}" contests: List[PlaintextBallotContest] = [] for contest in internal_manifest.get_contests_for(ballot_style.object_id): contests.append( self.get_random_contest_from( contest, Random(), allow_null_votes=allow_null_votes, allow_under_votes=allow_under_votes, ) ) ballots.append(PlaintextBallot(ballot_id, ballot_style.object_id, contests)) return ballots def get_simple_ballot_from_file(self) -> PlaintextBallot: return self._get_ballot_from_file(self.simple_ballot_filename) def get_simple_ballots_from_file(self) -> List[PlaintextBallot]: return self._get_ballots_from_file(self.simple_ballots_filename) @staticmethod def _get_ballot_from_file(filename: str) -> PlaintextBallot: return from_file(PlaintextBallot, os.path.join(_data, filename)) @staticmethod def _get_ballots_from_file(filename: str) -> List[PlaintextBallot]: return from_list_in_file(PlaintextBallot, os.path.join(_data, filename)) # TODO Migrate to strategies @composite def get_selection_well_formed( draw: _DrawType, ids: Any = uuids(), bools: Any = booleans(), txt: Any = text(), vote: Any = integers(0, 1), ) -> Tuple[str, PlaintextBallotSelection]: use_none = draw(bools) if use_none: extended_data = None else: extended_data = draw(txt) object_id = f"selection-{draw(ids)}" return ( object_id, PlaintextBallotSelection(object_id, draw(vote), draw(bools), extended_data), ) # TODO Migrate to strategies @composite def get_selection_poorly_formed( draw: _DrawType, ids: Any = uuids(), bools: Any = booleans(), txt: Any = text(), vote: Any = integers(0, 1), ) -> Tuple[str, PlaintextBallotSelection]: use_none = draw(bools) if use_none: extended_data = None else: extended_data = draw(txt) object_id = f"selection-{draw(ids)}" return ( object_id, PlaintextBallotSelection(object_id, draw(vote), draw(bools), extended_data), ) ================================================ FILE: src/electionguard_tools/factories/election_factory.py ================================================ """Factory to create elections for testing purposes.""" from datetime import datetime import os from dataclasses import dataclass from typing import Any, TypeVar, Callable, Optional, Tuple, List from hypothesis.strategies import ( composite, emails, integers, text, uuids, SearchStrategy, ) from electionguard.ballot import PlaintextBallot from electionguard.constants import ElectionConstants, get_constants from electionguard.election import CiphertextElectionContext from electionguard.elgamal import ElGamalPublicKey from electionguard.encrypt import EncryptionDevice, contest_from, generate_device_uuid from electionguard.group import TWO_MOD_Q from electionguard.guardian import Guardian, GuardianRecord from electionguard.key_ceremony import CeremonyDetails from electionguard.key_ceremony_mediator import KeyCeremonyMediator from electionguard.manifest import ( BallotStyle, Manifest, ElectionType, InternalManifest, SpecVersion, generate_placeholder_selections_from, GeopoliticalUnit, Candidate, Party, ContestDescription, SelectionDescription, ReportingUnitType, VoteVariationType, contest_description_with_placeholders_from, CandidateContestDescription, ReferendumContestDescription, ) from electionguard.serialize import from_file from electionguard.utils import get_optional from electionguard_tools.helpers.key_ceremony_orchestrator import ( KeyCeremonyOrchestrator, ) from electionguard_tools.helpers.election_builder import ElectionBuilder _T = TypeVar("_T") _DrawType = Callable[[SearchStrategy[_T]], _T] _data = os.path.realpath(os.path.join(__file__, "../../../../data")) NUMBER_OF_GUARDIANS = 5 QUORUM = 3 @dataclass class AllPublicElectionData: """All public data for election""" manifest: Manifest internal_manifest: InternalManifest context: CiphertextElectionContext constants: ElectionConstants guardians: List[GuardianRecord] @dataclass class AllPrivateElectionData: """All private data for election.""" guardians: List[Guardian] class ElectionFactory: """Factory to create elections.""" simple_election_manifest_file_name = "election_manifest_simple.json" def get_simple_manifest_from_file(self) -> Manifest: """Get simple manifest from json file.""" return self._get_manifest_from_file(self.simple_election_manifest_file_name) def get_manifest_from_filename(self, filename: str) -> Manifest: """Get simple manifest from json file.""" return self._get_manifest_from_file(filename) @staticmethod def get_manifest_from_file(spec_version: str, sample_manifest: str) -> Manifest: """Get simple manifest from json file.""" return from_file( Manifest, os.path.join( _data, spec_version, "sample", sample_manifest, "election_record", "manifest.json", ), ) @staticmethod def get_hamilton_manifest_from_file() -> Manifest: """Get Hamilton County manifest from json file.""" return from_file( Manifest, os.path.join(_data, os.path.join(_data, "manifest-hamilton-general.json")), ) def get_sample_manifest_with_encryption_context( self, sample_manifest: str ) -> Tuple[AllPublicElectionData, AllPrivateElectionData]: """Get hamilton manifest and context""" guardians: List[Guardian] = [] guardian_records: List[GuardianRecord] = [] # Configure the election builder manifest = self.get_manifest_from_filename(f"manifest-{sample_manifest}.json") builder = ElectionBuilder(NUMBER_OF_GUARDIANS, QUORUM, manifest) # Run the Key Ceremony ceremony_details = CeremonyDetails(NUMBER_OF_GUARDIANS, QUORUM) guardians = KeyCeremonyOrchestrator.create_guardians(ceremony_details) mediator = KeyCeremonyMediator("key-ceremony-mediator", ceremony_details) KeyCeremonyOrchestrator.perform_full_ceremony(guardians, mediator) # Final: Joint Key joint_key = mediator.publish_joint_key() # Publish Guardian Records guardian_records = [guardian.publish() for guardian in guardians] builder.set_public_key(get_optional(joint_key).joint_public_key) builder.set_commitment_hash(get_optional(joint_key).commitment_hash) internal_manifest, context = get_optional(builder.build()) constants = get_constants() return ( AllPublicElectionData( manifest, internal_manifest, context, constants, guardian_records, ), AllPrivateElectionData(guardians), ) @staticmethod def get_fake_manifest() -> Manifest: """Get a single fake manifest object that is manually constructed with default values.""" fake_ballot_style = BallotStyle("some-ballot-style-id") fake_ballot_style.geopolitical_unit_ids = ["some-geopoltical-unit-id"] fake_referendum_ballot_selections = [ # Referendum selections are simply a special case of `candidate` in the object model SelectionDescription( "some-object-id-affirmative", 0, "some-candidate-id-1" ), SelectionDescription("some-object-id-negative", 1, "some-candidate-id-2"), ] sequence_order = 0 number_elected = 1 votes_allowed = 1 fake_referendum_contest = ReferendumContestDescription( "some-referendum-contest-object-id", sequence_order, "some-geopoltical-unit-id", VoteVariationType.one_of_m, number_elected, votes_allowed, "some-referendum-contest-name", fake_referendum_ballot_selections, ) fake_candidate_ballot_selections = [ SelectionDescription( "some-object-id-candidate-1", 0, "some-candidate-id-1" ), SelectionDescription( "some-object-id-candidate-2", 1, "some-candidate-id-2" ), SelectionDescription( "some-object-id-candidate-3", 2, "some-candidate-id-3" ), ] sequence_order_2 = 1 number_elected_2 = 2 votes_allowed_2 = 2 fake_candidate_contest = CandidateContestDescription( "some-candidate-contest-object-id", sequence_order_2, "some-geopoltical-unit-id", VoteVariationType.one_of_m, number_elected_2, votes_allowed_2, "some-candidate-contest-name", fake_candidate_ballot_selections, ) fake_manifest = Manifest( spec_version=SpecVersion.EG0_95, election_scope_id="some-scope-id", type=ElectionType.unknown, start_date=datetime.now(), end_date=datetime.now(), geopolitical_units=[ GeopoliticalUnit( "some-geopoltical-unit-id", "some-gp-unit-name", ReportingUnitType.unknown, ) ], parties=[Party("some-party-id-1"), Party("some-party-id-2")], candidates=[ Candidate("some-candidate-id-1"), Candidate("some-candidate-id-2"), Candidate("some-candidate-id-3"), ], contests=[fake_referendum_contest, fake_candidate_contest], ballot_styles=[fake_ballot_style], ) return fake_manifest @staticmethod def get_fake_ciphertext_election( manifest: Manifest, elgamal_public_key: ElGamalPublicKey ) -> Tuple[InternalManifest, CiphertextElectionContext]: """Get mock election.""" builder = ElectionBuilder(number_of_guardians=1, quorum=1, manifest=manifest) builder.set_public_key(elgamal_public_key) builder.set_commitment_hash(TWO_MOD_Q) internal_manifest, context = get_optional(builder.build()) return internal_manifest, context # TODO: Move to ballot Factory? def get_fake_ballot( self, manifest: Optional[Manifest] = None, ballot_id: Optional[str] = None ) -> PlaintextBallot: """Get a single mock Ballot object that is manually constructed with default values.""" if manifest is None: manifest = self.get_fake_manifest() if ballot_id is None: ballot_id = "some-unique-ballot-id-123" fake_ballot = PlaintextBallot( ballot_id, manifest.ballot_styles[0].object_id, [contest_from(manifest.contests[0]), contest_from(manifest.contests[1])], ) return fake_ballot @staticmethod def _get_manifest_from_file(filename: str) -> Manifest: return from_file(Manifest, os.path.join(_data, filename)) @staticmethod def get_encryption_device() -> EncryptionDevice: """Get mock encryption device.""" return EncryptionDevice( generate_device_uuid(), 12345, 45678, "polling-place", ) @composite def get_selection_description_well_formed( draw: _DrawType, ints: Any = integers(1, 20), email_addresses: Any = emails(), candidate_id: Optional[str] = None, sequence_order: Optional[int] = None, ids: Any = uuids(), ) -> Tuple[str, SelectionDescription]: """Get mock well formed selection description.""" if candidate_id is None: candidate_id = draw(email_addresses) if sequence_order is None: sequence_order = draw(ints) object_id = f"{candidate_id}-selection-{draw(ids)}" return (object_id, SelectionDescription(object_id, sequence_order, candidate_id)) @composite def get_contest_description_well_formed( draw: _DrawType, ints: Any = integers(1, 20), txt: Any = text(), email_addresses: Any = emails(), selections: Any = get_selection_description_well_formed(), sequence_order: Optional[int] = None, electoral_district_id: Optional[str] = None, ) -> Tuple[str, ContestDescription]: """Get mock well formed selection contest.""" object_id = f"{draw(email_addresses)}-contest" if sequence_order is None: sequence_order = draw(ints) if electoral_district_id is None: electoral_district_id = f"{draw(email_addresses)}-gp-unit" first_int = draw(ints) second_int = draw(ints) # TODO ISSUE #33: support more votes than seats for other VoteVariationType options number_elected = min(first_int, second_int) votes_allowed = number_elected selection_descriptions: List[SelectionDescription] = [] for i in range(max(first_int, second_int)): selection: Tuple[str, SelectionDescription] = draw(selections) _, selection_description = selection selection_description.sequence_order = i selection_descriptions.append(selection_description) contest_description = ContestDescription( object_id, sequence_order, electoral_district_id, VoteVariationType.n_of_m, number_elected, votes_allowed, draw(txt), selection_descriptions, ) placeholder_selections = generate_placeholder_selections_from( contest_description, number_elected ) return ( object_id, contest_description_with_placeholders_from( contest_description, placeholder_selections ), ) ================================================ FILE: src/electionguard_tools/helpers/__init__.py ================================================ from electionguard_tools.helpers import election_builder from electionguard_tools.helpers import export from electionguard_tools.helpers import key_ceremony_orchestrator from electionguard_tools.helpers import tally_accumulate from electionguard_tools.helpers import tally_ceremony_orchestrator from electionguard_tools.helpers.election_builder import ( ElectionBuilder, ) from electionguard_tools.helpers.export import ( CIPHERTEXT_BALLOT_PREFIX, COEFFICIENTS_FILE_NAME, CONSTANTS_FILE_NAME, CONTEXT_FILE_NAME, DEVICES_DIR, DEVICE_PREFIX, ELECTION_RECORD_DIR, ENCRYPTED_TALLY_FILE_NAME, GUARDIANS_DIR, GUARDIAN_PREFIX, MANIFEST_FILE_NAME, PLAINTEXT_BALLOT_PREFIX, PRIVATE_DATA_DIR, PRIVATE_GUARDIAN_PREFIX, SPOILED_BALLOTS_DIR, SPOILED_BALLOT_PREFIX, SUBMITTED_BALLOTS_DIR, SUBMITTED_BALLOT_PREFIX, TALLY_FILE_NAME, export_private_data, export_record, ) from electionguard_tools.helpers.key_ceremony_orchestrator import ( KeyCeremonyOrchestrator, ) from electionguard_tools.helpers.tally_accumulate import ( accumulate_plaintext_ballots, ) from electionguard_tools.helpers.tally_ceremony_orchestrator import ( TallyCeremonyOrchestrator, ) __all__ = [ "CIPHERTEXT_BALLOT_PREFIX", "COEFFICIENTS_FILE_NAME", "CONSTANTS_FILE_NAME", "CONTEXT_FILE_NAME", "DEVICES_DIR", "DEVICE_PREFIX", "ELECTION_RECORD_DIR", "ENCRYPTED_TALLY_FILE_NAME", "ElectionBuilder", "GUARDIANS_DIR", "GUARDIAN_PREFIX", "KeyCeremonyOrchestrator", "MANIFEST_FILE_NAME", "PLAINTEXT_BALLOT_PREFIX", "PRIVATE_DATA_DIR", "PRIVATE_GUARDIAN_PREFIX", "SPOILED_BALLOTS_DIR", "SPOILED_BALLOT_PREFIX", "SUBMITTED_BALLOTS_DIR", "SUBMITTED_BALLOT_PREFIX", "TALLY_FILE_NAME", "TallyCeremonyOrchestrator", "accumulate_plaintext_ballots", "election_builder", "export", "export_private_data", "export_record", "key_ceremony_orchestrator", "tally_accumulate", "tally_ceremony_orchestrator", ] ================================================ FILE: src/electionguard_tools/helpers/election_builder.py ================================================ from __future__ import annotations from dataclasses import dataclass, field from typing import Dict, Optional, Tuple from electionguard.elgamal import ElGamalPublicKey from electionguard.election import ( CiphertextElectionContext, make_ciphertext_election_context, ) from electionguard.group import ElementModQ from electionguard.manifest import Manifest, InternalManifest from electionguard.utils import get_optional @dataclass class ElectionBuilder: """ `ElectionBuilder` is a stateful builder object that constructs `CiphertextElectionContext` objects following the initialization process that ElectionGuard Expects. """ number_of_guardians: int """ The number of guardians necessary to generate the public key """ quorum: int """ The quorum of guardians necessary to decrypt an election. Must be fewer than `number_of_guardians` """ manifest: Manifest internal_manifest: InternalManifest = field(init=False) election_key: Optional[ElGamalPublicKey] = field(default=None) commitment_hash: Optional[ElementModQ] = field(default=None) extended_data: Optional[Dict[str, str]] = field(default=None) def __post_init__(self) -> None: self.internal_manifest = InternalManifest(self.manifest) def set_public_key( self, election_joint_public_key: ElGamalPublicKey ) -> ElectionBuilder: """ Set election public key :param election_joint_public_key: elgamal public key for election :return: election builder """ self.election_key = election_joint_public_key return self def set_commitment_hash(self, commitment_hash: ElementModQ) -> ElectionBuilder: """ Set commitment hash :param commitment_hash: hash of the commitments guardians make to each other :return: election builder """ self.commitment_hash = commitment_hash return self def add_extended_data_field(self, name: str, value: str) -> ElectionBuilder: """ Set extended data field :param name: name of the extended data entry to add :param value: value of the extended data entry """ if self.extended_data is None: self.extended_data = {} self.extended_data[name] = value return self def build( self, ) -> Optional[Tuple[InternalManifest, CiphertextElectionContext]]: """ Build election :return: election manifest and context or none """ if not self.manifest.is_valid(): return None if self.election_key is None: return None return ( self.internal_manifest, make_ciphertext_election_context( self.number_of_guardians, self.quorum, get_optional(self.election_key), get_optional(self.commitment_hash), self.manifest.crypto_hash(), extended_data=self.extended_data, ), ) ================================================ FILE: src/electionguard_tools/helpers/export.py ================================================ """ Sample generation tool to export data from the election. Specifically constructed to assist with creating sample data. The export here is by no means exhaustive or prescriptive of how one may choose to export the data for publishing the election. Refer to the ElectionGuard spec for any specifics. """ from os import path from typing import Iterable from electionguard.ballot import PlaintextBallot, CiphertextBallot, SubmittedBallot from electionguard.constants import ElectionConstants from electionguard.election_polynomial import LagrangeCoefficientsRecord from electionguard.guardian import GuardianRecord, PrivateGuardianRecord from electionguard.election import CiphertextElectionContext from electionguard.encrypt import EncryptionDevice from electionguard.manifest import Manifest from electionguard.serialize import to_file from electionguard.tally import PlaintextTally, PublishedCiphertextTally # Public ELECTION_RECORD_DIR = "election_record" DEVICES_DIR = "encryption_devices" GUARDIANS_DIR = "guardians" SUBMITTED_BALLOTS_DIR = "submitted_ballots" SPOILED_BALLOTS_DIR = "spoiled_ballots" MANIFEST_FILE_NAME = "manifest" CONTEXT_FILE_NAME = "context" CONSTANTS_FILE_NAME = "constants" COEFFICIENTS_FILE_NAME = "coefficients" ENCRYPTED_TALLY_FILE_NAME = "encrypted_tally" TALLY_FILE_NAME = "tally" SUBMITTED_BALLOT_PREFIX = "submitted_ballot_" SPOILED_BALLOT_PREFIX = "spoiled_ballot_" DEVICE_PREFIX = "device_" GUARDIAN_PREFIX = "guardian_" # Private PRIVATE_DATA_DIR = "election_private_data" PLAINTEXT_BALLOT_PREFIX = "plaintext_ballot_" CIPHERTEXT_BALLOT_PREFIX = "ciphertext_ballot_" PRIVATE_GUARDIAN_PREFIX = "private_guardian_" # TODO #148 Revert PlaintextTally to PublishedPlaintextTally after moving spoiled info def export_record( manifest: Manifest, context: CiphertextElectionContext, constants: ElectionConstants, devices: Iterable[EncryptionDevice], submitted_ballots: Iterable[SubmittedBallot], spoiled_ballots: Iterable[PlaintextTally], ciphertext_tally: PublishedCiphertextTally, plaintext_tally: PlaintextTally, guardian_records: Iterable[GuardianRecord], lagrange_coefficients: LagrangeCoefficientsRecord, election_record_directory: str = ELECTION_RECORD_DIR, ) -> None: """Export a publishable election record""" devices_directory = path.join(election_record_directory, DEVICES_DIR) guardian_directory = path.join(election_record_directory, GUARDIANS_DIR) ballots_directory = path.join(election_record_directory, SUBMITTED_BALLOTS_DIR) spoiled_directory = path.join(election_record_directory, SPOILED_BALLOTS_DIR) to_file(manifest, MANIFEST_FILE_NAME, election_record_directory) to_file(context, CONTEXT_FILE_NAME, election_record_directory) to_file(constants, CONSTANTS_FILE_NAME, election_record_directory) to_file(lagrange_coefficients, COEFFICIENTS_FILE_NAME, election_record_directory) for device in devices: to_file(device, DEVICE_PREFIX + str(device.device_id), devices_directory) if guardian_records is not None: for guardian_record in guardian_records: to_file( guardian_record, GUARDIAN_PREFIX + guardian_record.guardian_id, guardian_directory, ) for ballot in submitted_ballots: to_file(ballot, SUBMITTED_BALLOT_PREFIX + ballot.object_id, ballots_directory) for spoiled_ballot in spoiled_ballots: to_file( spoiled_ballot, SPOILED_BALLOT_PREFIX + spoiled_ballot.object_id, spoiled_directory, ) to_file(ciphertext_tally, ENCRYPTED_TALLY_FILE_NAME, election_record_directory) to_file(plaintext_tally, TALLY_FILE_NAME, election_record_directory) def export_private_data( plaintext_ballots: Iterable[PlaintextBallot], ciphertext_ballots: Iterable[CiphertextBallot], private_guardian_records: Iterable[PrivateGuardianRecord], private_directory: str = PRIVATE_DATA_DIR, ) -> None: """Export non-publishable private data for an election. Useful for generating sample data sets. WARNING: DO NOT USE this in a production application. """ gaurdians_directory = path.join(private_directory, "private_guardians") plaintext_ballots_directory = path.join(private_directory, "plaintext_ballots") encrypted_ballots_directory = path.join(private_directory, "ciphertext_ballots") for private_guardian_record in private_guardian_records: to_file( private_guardian_record, PRIVATE_GUARDIAN_PREFIX + private_guardian_record.guardian_id, gaurdians_directory, ) for plaintext_ballot in plaintext_ballots: to_file( plaintext_ballot, PLAINTEXT_BALLOT_PREFIX + plaintext_ballot.object_id, plaintext_ballots_directory, ) for ciphertext_ballot in ciphertext_ballots: to_file( ciphertext_ballot, CIPHERTEXT_BALLOT_PREFIX + ciphertext_ballot.object_id, encrypted_ballots_directory, ) ================================================ FILE: src/electionguard_tools/helpers/key_ceremony_orchestrator.py ================================================ from typing import List from electionguard.guardian import Guardian from electionguard.key_ceremony import CeremonyDetails, ElectionPartialKeyVerification from electionguard.key_ceremony_mediator import GuardianPair, KeyCeremonyMediator from electionguard.utils import get_optional class KeyCeremonyOrchestrator: """Helper to assist in the key ceremony particularly for testing""" @staticmethod def create_guardians(ceremony_details: CeremonyDetails) -> List[Guardian]: return [ Guardian.from_nonce( str(i + 1), i + 1, ceremony_details.number_of_guardians, ceremony_details.quorum, ) for i in range(ceremony_details.number_of_guardians) ] @staticmethod def perform_full_ceremony( guardians: List[Guardian], mediator: KeyCeremonyMediator ) -> None: """Perform full key ceremony so joint election key is ready for publish""" KeyCeremonyOrchestrator.perform_round_1(guardians, mediator) KeyCeremonyOrchestrator.perform_round_2(guardians, mediator) KeyCeremonyOrchestrator.perform_round_3(guardians, mediator) @staticmethod def perform_round_1( guardians: List[Guardian], mediator: KeyCeremonyMediator ) -> None: """Perform Round 1 including announcing guardians and sharing public keys""" for guardian in guardians: mediator.announce(guardian.share_key()) for guardian in guardians: other_guardian_keys = get_optional(mediator.share_announced(guardian.id)) for guardian_key in other_guardian_keys: guardian.save_guardian_key(guardian_key) @staticmethod def perform_round_2( guardians: List[Guardian], mediator: KeyCeremonyMediator ) -> None: """Perform Round 2 including generating backups and sharing backups""" for guardian in guardians: guardian.generate_election_partial_key_backups() mediator.receive_backups(guardian.share_election_partial_key_backups()) for guardian in guardians: backups = get_optional(mediator.share_backups(guardian.id)) for backup in backups: guardian.save_election_partial_key_backup(backup) @staticmethod def perform_round_3( guardians: List[Guardian], mediator: KeyCeremonyMediator ) -> None: """Perform Round 3 including verifying backups""" for guardian in guardians: for other_guardian in guardians: verifications = [] if guardian.id is not other_guardian.id: verifications.append( get_optional( guardian.verify_election_partial_key_backup( other_guardian.id, ) ) ) mediator.receive_backup_verifications(verifications) @staticmethod def fail_round_3( guardians: List[Guardian], mediator: KeyCeremonyMediator ) -> GuardianPair: """Perform Round 3 including verifying backups but fail a single backup""" failing_guardian_pair = GuardianPair(guardians[0].id, guardians[1].id) for guardian in guardians: for other_guardian in guardians: verifications = [] if guardian.id is not other_guardian.id: verification = get_optional( guardian.verify_election_partial_key_backup(other_guardian.id) ) if ( verification.owner_id is failing_guardian_pair.owner_id and verification.designated_id is failing_guardian_pair.designated_id ): verification = ElectionPartialKeyVerification( failing_guardian_pair.owner_id, failing_guardian_pair.designated_id, failing_guardian_pair.designated_id, False, ) verifications.append(verification) mediator.receive_backup_verifications(verifications) return failing_guardian_pair ================================================ FILE: src/electionguard_tools/helpers/tally_accumulate.py ================================================ from typing import List, Dict from electionguard.ballot import PlaintextBallot def accumulate_plaintext_ballots(ballots: List[PlaintextBallot]) -> Dict[str, int]: """ Internal helper function for testing: takes a list of plaintext ballots as input, digs into all of the individual selections and then accumulates them, using their `object_id` fields as keys. This function only knows what to do with `n_of_m` elections. It's not a general-purpose tallying mechanism for other election types. :param ballots: a list of plaintext ballots :return: a dict from selection object_id's to integer totals """ tally: Dict[str, int] = {} for ballot in ballots: for contest in ballot.contests: for selection in contest.ballot_selections: assert ( not selection.is_placeholder_selection ), "Placeholder selections should not exist in the plaintext ballots" desc_id = selection.object_id if desc_id not in tally: tally[desc_id] = 0 # returns 1 or 0 for n-of-m ballot selections tally[desc_id] += selection.vote return tally ================================================ FILE: src/electionguard_tools/helpers/tally_ceremony_orchestrator.py ================================================ from typing import List, Optional from electionguard.ballot import SubmittedBallot from electionguard.election import CiphertextElectionContext from electionguard.guardian import Guardian, get_valid_ballot_shares from electionguard.decryption_mediator import DecryptionMediator from electionguard.key_ceremony import ElectionPublicKey from electionguard.tally import CiphertextTally from electionguard.utils import get_optional class TallyCeremonyOrchestrator: """Helper to assist in the decryption process particularly for testing""" @staticmethod def perform_decryption_setup( available_guardians: List[Guardian], mediator: DecryptionMediator, context: CiphertextElectionContext, ciphertext_tally: CiphertextTally, submitted_ballots: Optional[List[SubmittedBallot]] = None, ) -> None: """ Perform the necessary setup to ensure that a mediator can decrypt with all guardians available """ TallyCeremonyOrchestrator.announcement( available_guardians, [guardian.share_key() for guardian in available_guardians], mediator, context, ciphertext_tally, submitted_ballots, ) @staticmethod def perform_compensated_decryption_setup( available_guardians: List[Guardian], all_guardians_keys: List[ElectionPublicKey], mediator: DecryptionMediator, context: CiphertextElectionContext, ciphertext_tally: CiphertextTally, submitted_ballots: Optional[List[SubmittedBallot]] = None, ) -> None: """ Perform the necessary setup to ensure that a mediator can decrypt when there are guardians missing """ TallyCeremonyOrchestrator.announcement( available_guardians, all_guardians_keys, mediator, context, ciphertext_tally, submitted_ballots, ) TallyCeremonyOrchestrator.exchange_compensated_decryption_shares( available_guardians, mediator, context, ciphertext_tally, submitted_ballots ) @staticmethod def announcement( available_guardians: List[Guardian], all_guardians_keys: List[ElectionPublicKey], mediator: DecryptionMediator, context: CiphertextElectionContext, ciphertext_tally: CiphertextTally, submitted_ballots: Optional[List[SubmittedBallot]] = None, ) -> None: """ Each available guardian announces their presence. The missing guardians are also announced """ if submitted_ballots is None: submitted_ballots = [] # Announce available guardians for available_guardian in available_guardians: guardian_key = available_guardian.share_key() tally_share = get_optional( available_guardian.compute_tally_share(ciphertext_tally, context) ) ballot_shares = get_valid_ballot_shares( available_guardian.compute_ballot_shares(submitted_ballots, context) ) mediator.announce(guardian_key, tally_share, ballot_shares) # type: ignore # Announce missing guardians # Get all guardian keys and filter to determine the missing guardians available_guardian_ids = [guardian.id for guardian in available_guardians] missing_guardians = [ key for key in all_guardians_keys if key.owner_id not in available_guardian_ids ] for missing_guardian_key in missing_guardians: mediator.announce_missing(missing_guardian_key) @staticmethod def exchange_compensated_decryption_shares( available_guardians: List[Guardian], mediator: DecryptionMediator, context: CiphertextElectionContext, ciphertext_tally: CiphertextTally, submitted_ballots: Optional[List[SubmittedBallot]] = None, ) -> None: """ Available guardians generate the compensated decryption shares for the missing guardians and send to the mediator. """ if submitted_ballots is None: submitted_ballots = [] # Exchange compensated shares missing_guardians = mediator.get_missing_guardians() for available_guardian in available_guardians: for missing_guardian in missing_guardians: tally_share = available_guardian.compute_compensated_tally_share( missing_guardian.owner_id, ciphertext_tally, context, ) if tally_share is not None: mediator.receive_tally_compensation_share(tally_share) ballot_shares = get_valid_ballot_shares( available_guardian.compute_compensated_ballot_shares( missing_guardian.owner_id, submitted_ballots, context, ) ) mediator.receive_ballot_compensation_shares(ballot_shares) # Combine compensated shares into decryption share for missing guardians mediator.reconstruct_shares_for_tally(ciphertext_tally) mediator.reconstruct_shares_for_ballots(submitted_ballots) ================================================ FILE: src/electionguard_tools/scripts/__init__.py ================================================ from electionguard_tools.scripts import sample_generator from electionguard_tools.scripts.sample_generator import ( DEFAULT_NUMBER_OF_BALLOTS, DEFAULT_SAMPLE_MANIFEST, DEFAULT_SPEC_VERSION, DEFAULT_SPOIL_RATE, DEFAULT_USE_ALL_GUARDIANS, DEFAULT_USE_PRIVATE_DATA, ElectionSampleDataGenerator, ) __all__ = [ "DEFAULT_NUMBER_OF_BALLOTS", "DEFAULT_SAMPLE_MANIFEST", "DEFAULT_SPEC_VERSION", "DEFAULT_SPOIL_RATE", "DEFAULT_USE_ALL_GUARDIANS", "DEFAULT_USE_PRIVATE_DATA", "ElectionSampleDataGenerator", "sample_generator", ] ================================================ FILE: src/electionguard_tools/scripts/sample_generator.py ================================================ #!/usr/bin/env python from random import randint from shutil import rmtree from typing import List from electionguard.ballot import ( BallotBoxState, CiphertextBallot, SubmittedBallot, ) from electionguard.data_store import DataStore from electionguard.ballot_box import BallotBox, get_ballots from electionguard.decryption_mediator import DecryptionMediator from electionguard.election_polynomial import LagrangeCoefficientsRecord from electionguard.encrypt import ( EncryptionDevice, EncryptionMediator, ) from electionguard.guardian import PrivateGuardianRecord from electionguard.tally import tally_ballots from electionguard.type import BallotId from electionguard.utils import get_optional from electionguard_tools.factories.ballot_factory import BallotFactory from electionguard_tools.factories.election_factory import ElectionFactory, QUORUM from electionguard_tools.helpers.tally_ceremony_orchestrator import ( TallyCeremonyOrchestrator, ) from electionguard_tools.helpers.export import ( export_record, export_private_data, ELECTION_RECORD_DIR, PRIVATE_DATA_DIR, ) DEFAULT_NUMBER_OF_BALLOTS = 5 DEFAULT_SPOIL_RATE = 50 DEFAULT_USE_ALL_GUARDIANS = False DEFAULT_USE_PRIVATE_DATA = False DEFAULT_SPEC_VERSION = "1.0" DEFAULT_SAMPLE_MANIFEST = "hamilton-general" class ElectionSampleDataGenerator: """ Generates sample data for an example election using the "Hamilton County" data set. """ election_factory: ElectionFactory ballot_factory: BallotFactory encryption_device: EncryptionDevice encrypter: EncryptionMediator def __init__(self) -> None: """Initialize the class""" self.election_factory = ElectionFactory() self.ballot_factory = BallotFactory() self.encryption_device = self.election_factory.get_encryption_device() def generate( self, number_of_ballots: int = DEFAULT_NUMBER_OF_BALLOTS, spoil_rate: int = DEFAULT_SPOIL_RATE, use_all_guardians: bool = DEFAULT_USE_ALL_GUARDIANS, use_private_data: bool = DEFAULT_USE_PRIVATE_DATA, sample_manifest: str = DEFAULT_SAMPLE_MANIFEST, ) -> None: """ Generate the sample data set """ # Clear the results directory rmtree(ELECTION_RECORD_DIR, ignore_errors=True) rmtree(PRIVATE_DATA_DIR, ignore_errors=True) # Configure the election # TODO: pass the spec version and the manifest name in ( public_data, private_data, ) = self.election_factory.get_sample_manifest_with_encryption_context( sample_manifest ) plaintext_ballots = ( self.ballot_factory.generate_fake_plaintext_ballots_for_election( public_data.internal_manifest, number_of_ballots ) ) self.encrypter = EncryptionMediator( public_data.internal_manifest, public_data.context, self.encryption_device ) # Encrypt some ballots ciphertext_ballots: List[CiphertextBallot] = [] for plaintext_ballot in plaintext_ballots: ciphertext_ballots.append( get_optional(self.encrypter.encrypt(plaintext_ballot)) ) ballot_store: DataStore[BallotId, SubmittedBallot] = DataStore() ballot_box = BallotBox( public_data.internal_manifest, public_data.context, ballot_store ) # Randomly cast/spoil the ballots submitted_ballots: List[SubmittedBallot] = [] for ballot in ciphertext_ballots: if randint(0, 100) < spoil_rate: submitted_ballots.append(get_optional(ballot_box.spoil(ballot))) else: submitted_ballots.append(get_optional(ballot_box.cast(ballot))) # Tally spoiled_ciphertext_ballots = get_ballots(ballot_store, BallotBoxState.SPOILED) ciphertext_tally = get_optional( tally_ballots( ballot_store, public_data.internal_manifest, public_data.context ) ) # Decrypt mediator = DecryptionMediator("sample-manifest-decrypter", public_data.context) available_guardians = ( private_data.guardians if use_all_guardians else private_data.guardians[0:QUORUM] ) spoiled_ballots = list(spoiled_ciphertext_ballots.values()) if not use_all_guardians: available_guardians = private_data.guardians[0:QUORUM] all_guardian_keys = [ guardian.share_key() for guardian in private_data.guardians ] TallyCeremonyOrchestrator.perform_compensated_decryption_setup( available_guardians, all_guardian_keys, mediator, public_data.context, ciphertext_tally, spoiled_ballots, ) else: TallyCeremonyOrchestrator.perform_decryption_setup( available_guardians, mediator, public_data.context, ciphertext_tally, spoiled_ballots, ) plaintext_tally = mediator.get_plaintext_tally( ciphertext_tally, public_data.manifest ) plaintext_spoiled_ballots = list( get_optional( mediator.get_plaintext_ballots(spoiled_ballots, public_data.manifest) ).values() ) if plaintext_tally: export_record( public_data.manifest, public_data.context, public_data.constants, [self.encryption_device], submitted_ballots, plaintext_spoiled_ballots, ciphertext_tally.publish(), plaintext_tally, public_data.guardians, LagrangeCoefficientsRecord(mediator.get_lagrange_coefficients()), ) if use_private_data: export_private_data( plaintext_ballots, ciphertext_ballots, [ # pylint: disable=protected-access PrivateGuardianRecord( guardian.id, guardian._election_keys, guardian._backups_to_share, guardian._guardian_election_public_keys, guardian._guardian_election_partial_key_backups, guardian._guardian_election_partial_key_verifications, ) for guardian in private_data.guardians ], ) if __name__ == "__main__": import argparse parser = argparse.ArgumentParser( description="Generate sample ballot data", formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) parser.add_argument( "-m", "--manifest", metavar="<manifest>", default=DEFAULT_SAMPLE_MANIFEST, type=str, help="The manifest to use to generate sample data.", choices=["full", "hamilton-general", "minimal", "small"], ) parser.add_argument( "-n", "--number-of-ballots", metavar="<count>", default=DEFAULT_NUMBER_OF_BALLOTS, type=int, help="The number of ballots to generate.", ) parser.add_argument( "-s", "--spoil-rate", metavar="<rate>", default=DEFAULT_SPOIL_RATE, type=int, help="The approximate percentage of total ballots to spoil instead of cast. Provide a number from 0-100.", ) parser.add_argument( "-a", "--all-guardians", default=DEFAULT_USE_ALL_GUARDIANS, action="store_true", help="If specified, all guardians will be included. Otherwise, only the threshold number will be included.", ) parser.add_argument( "-p", "--private-data", default=DEFAULT_USE_PRIVATE_DATA, action="store_true", help="Include private data when generating.", ) parser.add_argument( "-v", "--version", default=DEFAULT_SPEC_VERSION, type=str, help="The spec version to use.", choices=[DEFAULT_SPEC_VERSION], ) args = parser.parse_args() ElectionSampleDataGenerator().generate( args.number_of_ballots, args.spoil_rate, args.all_guardians, args.private_data, args.manifest, ) ================================================ FILE: src/electionguard_tools/strategies/__init__.py ================================================ from electionguard_tools.strategies import election from electionguard_tools.strategies import elgamal from electionguard_tools.strategies import group from electionguard_tools.strategies.election import ( CiphertextElectionsTupleType, ElectionsAndBallotsTupleType, annotated_emails, annotated_strings, ballot_styles, candidate_contest_descriptions, candidates, ciphertext_elections, contact_infos, contest_descriptions, contest_descriptions_room_for_overvoting, election_descriptions, election_types, elections_and_ballots, geopolitical_units, human_names, internationalized_human_names, internationalized_texts, language_human_names, languages, party_lists, plaintext_voted_ballot, plaintext_voted_ballots, referendum_contest_descriptions, reporting_unit_types, two_letter_codes, ) from electionguard_tools.strategies.elgamal import ( elgamal_keypairs, ) from electionguard_tools.strategies.group import ( elements_mod_p, elements_mod_p_no_zero, elements_mod_q, elements_mod_q_no_zero, ) __all__ = [ "CiphertextElectionsTupleType", "ElectionsAndBallotsTupleType", "annotated_emails", "annotated_strings", "ballot_styles", "candidate_contest_descriptions", "candidates", "ciphertext_elections", "contact_infos", "contest_descriptions", "contest_descriptions_room_for_overvoting", "election", "election_descriptions", "election_types", "elections_and_ballots", "elements_mod_p", "elements_mod_p_no_zero", "elements_mod_q", "elements_mod_q_no_zero", "elgamal", "elgamal_keypairs", "geopolitical_units", "group", "human_names", "internationalized_human_names", "internationalized_texts", "language_human_names", "languages", "party_lists", "plaintext_voted_ballot", "plaintext_voted_ballots", "referendum_contest_descriptions", "reporting_unit_types", "two_letter_codes", ] ================================================ FILE: src/electionguard_tools/strategies/election.py ================================================ from copy import deepcopy from functools import reduce from random import Random from typing import Any, TypeVar, Callable, List, Optional, Tuple, Union from hypothesis.provisional import urls from hypothesis.strategies import ( composite, emails, integers, lists, SearchStrategy, text, uuids, datetimes, one_of, just, ) from hypothesis.strategies._internal.core import sampled_from from electionguard.ballot import PlaintextBallotContest, PlaintextBallot from electionguard.election import ( CiphertextElectionContext, make_ciphertext_election_context, ) from electionguard.elgamal import ElGamalKeyPair from electionguard.encrypt import selection_from from electionguard.group import ElementModQ from electionguard.manifest import ( Candidate, ElectionType, ReportingUnitType, SpecVersion, VoteVariationType, ContactInformation, GeopoliticalUnit, BallotStyle, Language, InternationalizedText, AnnotatedString, Party, CandidateContestDescription, ReferendumContestDescription, SelectionDescription, ContestDescription, Manifest, InternalManifest, ) from electionguard_tools.strategies.elgamal import elgamal_keypairs from electionguard_tools.strategies.group import elements_mod_q_no_zero _T = TypeVar("_T") _DrawType = Callable[[SearchStrategy[_T]], _T] _first_names = [ "James", "Mary", "John", "Patricia", "Robert", "Jennifer", "Michael", "Linda", "William", "Elizabeth", "David", "Barbara", "Richard", "Susan", "Joseph", "Jessica", "Thomas", "Sarah", "Charles", "Karen", "Christopher", "Nancy", "Daniel", "Margaret", "Matthew", "Lisa", "Anthony", "Betty", "Donald", "Dorothy", "Sylvia", "Viktor", "Camille", "Mirai", "Anant", "Rohan", "François", "Altuğ", "Sigurður", "Böðmóður", "Quang Dũng", ] _last_names = [ "SMITH", "JOHNSON", "WILLIAMS", "JONES", "BROWN", "DAVIS", "MILLER", "WILSON", "MOORE", "TAYLOR", "ANDERSON", "THOMAS", "JACKSON", "WHITE", "HARRIS", "MARTIN", "THOMPSON", "GARCIA", "MARTINEZ", "ROBINSON", "CLARK", "RODRIGUEZ", "LEWIS", "LEE", "WALKER", "HALL", "ALLEN", "YOUNG", "HERNANDEZ", "KING", "WRIGHT", "LOPEZ", "HILL", "SCOTT", "GREEN", "ADAMS", "BAKER", "GONZALEZ", "STEELE-LOY", "O'CONNOR", "ANAND", "PATEL", "GUPTA", "ĐẶNG", ] @composite def human_names(draw: _DrawType) -> str: """ Generates a string with a human first and last name. :param draw: Hidden argument, used by Hypothesis. """ return ( f"{_first_names[draw(integers(0, len(_first_names) - 1))]} " f"{_last_names[draw(integers(0, len(_last_names) - 1))]}" ) @composite def election_types(draw: _DrawType) -> ElectionType: """ Generates an `ElectionType`. :param draw: Hidden argument, used by Hypothesis. """ n = draw(sampled_from(ElectionType)) return ElectionType(n) @composite def reporting_unit_types(draw: _DrawType) -> ReportingUnitType: """ Generates a `ReportingUnitType` object. :param draw: Hidden argument, used by Hypothesis. """ n = draw(sampled_from(ReportingUnitType)) return ReportingUnitType(n) @composite def contact_infos(draw: _DrawType) -> ContactInformation: """ Generates a `ContactInformation` object. :param draw: Hidden argument, used by Hypothesis. """ return ContactInformation( None, draw(lists(annotated_emails(), min_size=1, max_size=3)), None, draw(human_names()), ) @composite def two_letter_codes(draw: _DrawType, min_size: int = 2, max_size: int = 2) -> Any: """ Generates a string with only a few characters, by default 2 letters from `a` to `z`, but configurable with the `min_size` and `max_size` parameters. Useful when you want something like a two-letter country or language code. :param draw: Hidden argument, used by Hypothesis. :param min_size: minimum number of characters to generate (default: 2) :param max_size: maximum number of characters to generate (default: 2) """ return draw( text( alphabet="abcdefghijklmnopqrstuvwxyz", min_size=min_size, max_size=max_size ) ) @composite def languages(draw: _DrawType) -> Language: """ Generates a `Language` object with an arbitrary two-letter string as the code and something messier for the text ostensibly written in that language. :param draw: Hidden argument, used by Hypothesis. """ return Language(draw(emails()), draw(two_letter_codes())) @composite def language_human_names(draw: _DrawType) -> Language: """ Generates a `Language` object with an arbitrary two-letter string as the code and a human name for the text ostensibly written in that language. :param draw: Hidden argument, used by Hypothesis. """ return Language(draw(human_names()), draw(two_letter_codes())) @composite def internationalized_texts(draw: _DrawType) -> InternationalizedText: """ Generates an `InternationalizedText` object with a list of `Language` objects within (representing a multilingual string). :param draw: Hidden argument, used by Hypothesis. """ return InternationalizedText(draw(lists(languages(), min_size=1, max_size=3))) @composite def internationalized_human_names(draw: _DrawType) -> InternationalizedText: """ Generates an `InternationalizedText` object with a list of `Language` objects within (representing a multilingual human name). :param draw: Hidden argument, used by Hypothesis. """ return InternationalizedText( draw(lists(language_human_names(), min_size=1, max_size=3)) ) @composite def annotated_strings(draw: _DrawType) -> AnnotatedString: """ Generates an `AnnotatedString` object with one `Language` and an associated `value` string. :param draw: Hidden argument, used by Hypothesis. """ s = draw(languages()) # We're just reusing the "value" string already associated with the language for now. return AnnotatedString(annotation=s.language, value=s.value) @composite def annotated_emails(draw: _DrawType) -> AnnotatedString: """ Generates a `Email` object with an arbitrary two-letter string as annotation and an email format string as value. :param draw: Hidden argument, used by Hypothesis. """ return AnnotatedString(draw(two_letter_codes()), draw(emails())) @composite def ballot_styles( draw: _DrawType, parties: List[Party], geo_units: List[GeopoliticalUnit] ) -> BallotStyle: """ Generates a `BallotStyle` object, which rolls up a list of parties and geopolitical units (passed as arguments), with some additional information added on as well. :param draw: Hidden argument, used by Hypothesis. :param party_ids: a list of `Party` objects to be used in this ballot style :param geo_units: a list of `GeopoliticalUnit` objects to be used in this ballot style """ assert len(parties) > 0 assert len(geo_units) > 0 gp_unit_ids = [x.object_id for x in geo_units] if len(gp_unit_ids) == 0: gp_unit_ids = [] party_ids = [party.get_party_id() for party in parties] if len(party_ids) == 0: party_ids = [] image_uri = draw(urls()) return BallotStyle(str(draw(uuids())), gp_unit_ids, party_ids, image_uri) @composite def party_lists(draw: _DrawType, num_parties: int) -> List[Party]: """ Generates a `List[Party]` of the requested length. :param draw: Hidden argument, used by Hypothesis. :param num_parties: Number of parties to generate in the list. """ party_names = [f"Party{n}" for n in range(num_parties)] party_abbrvs = [f"P{n}" for n in range(num_parties)] assert num_parties > 0 return [ Party( object_id=str(draw(uuids())), name=InternationalizedText([Language(party_names[i], "en")]), abbreviation=party_abbrvs[i], color=None, logo_uri=draw(urls()), ) for i in range(num_parties) ] @composite def geopolitical_units(draw: _DrawType) -> GeopoliticalUnit: """ Generates a `GeopoliticalUnit` object. :param draw: Hidden argument, used by Hypothesis. """ return GeopoliticalUnit( object_id=str(draw(uuids())), name=draw(emails()), type=draw(reporting_unit_types()), contact_information=draw(contact_infos()), ) @composite def candidates(draw: _DrawType, party_list: Optional[List[Party]]) -> Candidate: """ Generates a `Candidate` object, assigning it one of the parties from `party_list` at random, with a chance that there will be no party assigned at all. :param draw: Hidden argument, used by Hypothesis. :param party_list: A list of `Party` objects. If None, then the resulting `Candidate` will have no party. """ if party_list: party = party_list[draw(integers(0, len(party_list) - 1))] party_id = party.get_party_id() else: party_id = None return Candidate( str(draw(uuids())), draw(internationalized_human_names()), party_id, draw(one_of(just(None), urls())), ) def _candidate_to_selection_description( candidate: Candidate, sequence_order: int ) -> SelectionDescription: """ Given a `Candidate` and its position in a list of candidates, returns an equivalent `SelectionDescription`. The selection's `object_id` will contain the candidates's `object_id` within, but will have a "c-" prefix attached, so you'll be able to tell that they're related. """ return SelectionDescription( f"c-{candidate.object_id}", sequence_order, candidate.get_candidate_id() ) @composite def candidate_contest_descriptions( draw: _DrawType, sequence_order: int, party_list: List[Party], geo_units: List[GeopoliticalUnit], n: Optional[int] = None, m: Optional[int] = None, ) -> Tuple[List[Candidate], CandidateContestDescription]: """ Generates a tuple: a `List[Candidate]` and a corresponding `CandidateContestDescription` for an n-of-m contest. :param draw: Hidden argument, used by Hypothesis. :param sequence_order: integer describing the order of this contest; make these sequential when generating many contests. :param party_list: A list of `Party` objects; each candidate's party is drawn at random from this list. :param geo_units: A list of `GeopoliticalUnit`; one of these goes into the `electoral_district_id` :param n: optional integer, specifying a particular value for n in this n-of-m contest, otherwise it's varied by Hypothesis. :param m: optional integer, specifying a particular value for m in this n-of-m contest, otherwise it's varied by Hypothesis. """ if n is None: n = draw(integers(1, 3)) if m is None: m = n + draw(integers(0, 3)) # for an n-of-m election party_ids = [p.get_party_id() for p in party_list] contest_candidates = draw(lists(candidates(party_list), min_size=m, max_size=m)) selection_descriptions = [ _candidate_to_selection_description(contest_candidates[i], i) for i in range(m) ] vote_variation = VoteVariationType.one_of_m if n == 1 else VoteVariationType.n_of_m return ( contest_candidates, CandidateContestDescription( object_id=str(draw(uuids())), electoral_district_id=geo_units[ draw(integers(0, len(geo_units) - 1)) ].object_id, sequence_order=sequence_order, vote_variation=vote_variation, number_elected=n, votes_allowed=n, # should this be None or n? name=draw(emails()), ballot_selections=selection_descriptions, ballot_title=draw(internationalized_texts()), ballot_subtitle=draw(internationalized_texts()), primary_party_ids=party_ids, ), ) @composite def contest_descriptions_room_for_overvoting( draw: _DrawType, sequence_order: int, party_list: List[Party], geo_units: List[GeopoliticalUnit], ) -> Any: """ Similar to `contest_descriptions`, but guarantees that for the n-of-m contest that n < m, therefore it's possible to construct an "overvoted" plaintext, which should then fail subsequent tests. :param draw: Hidden argument, used by Hypothesis. :param sequence_order: integer describing the order of this contest; make these sequential when generating many contests. :param party_list: A list of `Party` objects; each candidate's party is drawn at random from this list. :param geo_units: A list of `GeopoliticalUnit`; one of these goes into the `electoral_district_id` """ n = draw(integers(1, 3)) m = n + draw(integers(1, 3)) return draw( candidate_contest_descriptions( sequence_order=sequence_order, party_list=party_list, geo_units=geo_units, n=n, m=m, ) ) @composite def referendum_contest_descriptions( draw: _DrawType, sequence_order: int, geo_units: List[GeopoliticalUnit] ) -> Tuple[List[Candidate], ReferendumContestDescription]: """ Generates a tuple: a list of party-less candidates and a corresponding `ReferendumContestDescription`. :param draw: Hidden argument, used by Hypothesis. :param sequence_order: integer describing the order of this contest; make these sequential when generating many contests. :param geo_units: A list of `GeopoliticalUnit`; one of these goes into the `electoral_district_id` """ n = draw(integers(1, 3)) contest_candidates = draw(lists(candidates(None), min_size=n, max_size=n)) selection_descriptions = [ _candidate_to_selection_description(contest_candidates[i], i) for i in range(n) ] return ( contest_candidates, ReferendumContestDescription( object_id=str(draw(uuids())), electoral_district_id=geo_units[ draw(integers(0, len(geo_units) - 1)) ].object_id, sequence_order=sequence_order, vote_variation=VoteVariationType.one_of_m, number_elected=1, votes_allowed=1, # should this be None or 1? name=draw(emails()), ballot_selections=selection_descriptions, ballot_title=draw(internationalized_texts()), ballot_subtitle=draw(internationalized_texts()), ), ) @composite def contest_descriptions( draw: _DrawType, sequence_order: int, party_list: List[Party], geo_units: List[GeopoliticalUnit], ) -> Any: """ Generates either the result of `referendum_contest_descriptions` or `candidate_contest_descriptions`. :param draw: Hidden argument, used by Hypothesis. :param sequence_order: integer describing the order of this contest; make these sequential when generating many contests. :param party_list: A list of `Party` objects; each candidate's party is drawn at random from this list. See `candidates` for details on this assignment. :param geo_units: A list of `GeopoliticalUnit`; one of these goes into the `electoral_district_id` """ return draw( one_of( referendum_contest_descriptions(sequence_order, geo_units), candidate_contest_descriptions(sequence_order, party_list, geo_units), ) ) @composite def election_descriptions( draw: _DrawType, max_num_parties: int = 3, max_num_contests: int = 3 ) -> Manifest: """ Generates a `Manifest` -- the top-level object describing an election. :param draw: Hidden argument, used by Hypothesis. :param max_num_parties: The largest number of parties that will be generated (default: 3) :param max_num_contests: The largest number of contests that will be generated (default: 3) """ assert max_num_parties > 0, "need at least one party" assert max_num_contests > 0, "need at least one contest" geo_units = [draw(geopolitical_units())] num_parties: int = draw(integers(1, max_num_parties)) # keep this small so tests run faster parties: List[Party] = draw(party_lists(num_parties)) num_contests: int = draw(integers(1, max_num_contests)) # generate a collection candidates mapped to contest descriptions candidate_contests: List[Tuple[List[Candidate], ContestDescription]] = [ draw(contest_descriptions(i, parties, geo_units)) for i in range(num_contests) ] assert len(candidate_contests) > 0 candidates_ = reduce( lambda a, b: a + b, [candidate_contest[0] for candidate_contest in candidate_contests], ) contests = [candidate_contest[1] for candidate_contest in candidate_contests] styles = [draw(ballot_styles(parties, geo_units))] # maybe later on we'll do something more complicated with dates start_date = draw(datetimes()) end_date = start_date return Manifest( election_scope_id=draw(emails()), spec_version=SpecVersion.EG0_95, type=ElectionType.general, # good enough for now start_date=start_date, end_date=end_date, geopolitical_units=geo_units, parties=parties, candidates=candidates_, contests=contests, ballot_styles=styles, name=draw(internationalized_texts()), contact_information=draw(contact_infos()), ) @composite def plaintext_voted_ballots( draw: _DrawType, internal_manifest: InternalManifest, count: int = 1 ) -> Union[Any, List[PlaintextBallot]]: """ Given """ if count == 1: return draw(plaintext_voted_ballot(internal_manifest)) ballots: List[PlaintextBallot] = [] for _i in range(count): ballots.append(draw(plaintext_voted_ballot(internal_manifest))) return ballots @composite def plaintext_voted_ballot( draw: _DrawType, internal_manifest: InternalManifest ) -> PlaintextBallot: """ Given an `InternalManifest` object, generates an arbitrary `PlaintextBallot` with the choices made randomly. :param draw: Hidden argument, used by Hypothesis. :param internal_manifest: Any `InternalManifest` """ num_ballot_styles = len(internal_manifest.ballot_styles) assert num_ballot_styles > 0, "invalid election with no ballot styles" # pick a ballot style at random ballot_style = internal_manifest.ballot_styles[ draw(integers(0, num_ballot_styles - 1)) ] contests = internal_manifest.get_contests_for(ballot_style.object_id) assert len(contests) > 0, "invalid ballot style with no contests in it" voted_contests: List[PlaintextBallotContest] = [] for contest in contests: assert contest.is_valid(), "every contest needs to be valid" n = contest.number_elected # we need exactly this many 1's, and the rest 0's ballot_selections = deepcopy(contest.ballot_selections) assert len(ballot_selections) >= n random = Random(draw(integers())) random.shuffle(ballot_selections) cut_point = draw(integers(0, n)) yes_votes = ballot_selections[:cut_point] no_votes = ballot_selections[cut_point:] voted_selections = [ selection_from(description, is_placeholder=False, is_affirmative=True) for description in yes_votes ] + [ selection_from(description, is_placeholder=False, is_affirmative=False) for description in no_votes ] voted_contests.append( PlaintextBallotContest(contest.object_id, voted_selections) ) return PlaintextBallot(str(draw(uuids())), ballot_style.object_id, voted_contests) CiphertextElectionsTupleType = Tuple[ElementModQ, CiphertextElectionContext] @composite def ciphertext_elections( draw: _DrawType, manifest: Manifest ) -> CiphertextElectionsTupleType: """ Generates a `CiphertextElectionContext` with a single public-private key pair as the encryption context. In a real election, the key ceremony would be used to generate a shared public key. :param draw: Hidden argument, used by Hypothesis. :param manifest: An `Manifest` object, with which the `CiphertextElectionContext` will be associated :return: a tuple of a `CiphertextElectionContext` and the secret key associated with it """ keypair: ElGamalKeyPair = draw(elgamal_keypairs()) secret_key = keypair.secret_key public_key = keypair.public_key commitment_hash = draw(elements_mod_q_no_zero()) ciphertext_election_with_secret: CiphertextElectionsTupleType = ( secret_key, make_ciphertext_election_context( number_of_guardians=1, quorum=1, elgamal_public_key=public_key, commitment_hash=commitment_hash, manifest_hash=manifest.crypto_hash(), ), ) return ciphertext_election_with_secret ElectionsAndBallotsTupleType = Tuple[ Manifest, InternalManifest, List[PlaintextBallot], ElementModQ, CiphertextElectionContext, ] @composite def elections_and_ballots( draw: _DrawType, num_ballots: int = 3 ) -> ElectionsAndBallotsTupleType: """ A convenience generator to generate all of the necessary components for simulating an election. Every ballot will match the same ballot style. Hypothesis doesn't let us declare a type hint on strategy return values, so you can use `ELECTIONS_AND_BALLOTS_TUPLE_TYPE`. :param draw: Hidden argument, used by Hypothesis. :param num_ballots: The number of ballots to generate (default: 3). :reeturn: a tuple of: an `InternalManifest`, a list of plaintext ballots, an ElGamal secret key, and a `CiphertextElectionContext` """ assert num_ballots >= 0, "You're asking for a negative number of ballots?" manifest = draw(election_descriptions()) internal_manifest = InternalManifest(manifest) ballots = [ draw(plaintext_voted_ballots(internal_manifest)) for _ in range(num_ballots) ] secret_key, context = draw(ciphertext_elections(manifest)) mock_election: ElectionsAndBallotsTupleType = ( manifest, internal_manifest, ballots, secret_key, context, ) return mock_election ================================================ FILE: src/electionguard_tools/strategies/elgamal.py ================================================ from typing import Optional, TypeVar, Callable from hypothesis.strategies import composite, SearchStrategy from electionguard.elgamal import ElGamalKeyPair, elgamal_keypair_from_secret from electionguard.group import ONE_MOD_Q, TWO_MOD_Q from electionguard_tools.strategies.group import elements_mod_q_no_zero _T = TypeVar("_T") _DrawType = Callable[[SearchStrategy[_T]], _T] @composite def elgamal_keypairs(draw: _DrawType) -> Optional[ElGamalKeyPair]: """ Generates an arbitrary ElGamal secret/public keypair. :param draw: Hidden argument, used by Hypothesis. """ e = draw(elements_mod_q_no_zero()) return elgamal_keypair_from_secret(e if e != ONE_MOD_Q else TWO_MOD_Q) ================================================ FILE: src/electionguard_tools/strategies/group.py ================================================ from typing import TypeVar, Callable from hypothesis.strategies import composite, integers, SearchStrategy from electionguard.constants import ( get_small_prime, get_large_prime, ) from electionguard.group import ( ElementModP, ElementModQ, ) _T = TypeVar("_T") _DrawType = Callable[[SearchStrategy[_T]], _T] @composite def elements_mod_q(draw: _DrawType) -> ElementModQ: """ Generates an arbitrary element from [0,Q). :param draw: Hidden argument, used by Hypothesis. """ return ElementModQ(draw(integers(min_value=0, max_value=get_small_prime() - 1))) @composite def elements_mod_q_no_zero(draw: _DrawType) -> ElementModQ: """ Generates an arbitrary element from [1,Q). :param draw: Hidden argument, used by Hypothesis. """ return ElementModQ(draw(integers(min_value=1, max_value=get_small_prime() - 1))) @composite def elements_mod_p(draw: _DrawType) -> ElementModP: """ Generates an arbitrary element from [0,P). :param draw: Hidden argument, used by Hypothesis. """ return ElementModP(draw(integers(min_value=0, max_value=get_large_prime() - 1))) @composite def elements_mod_p_no_zero(draw: _DrawType) -> ElementModP: """ Generates an arbitrary element from [1,P). :param draw: Hidden argument, used by Hypothesis. """ return ElementModP(draw(integers(min_value=1, max_value=get_large_prime() - 1))) ================================================ FILE: src/electionguard_verify/__init__.py ================================================ import importlib.metadata # <AUTOGEN_INIT> from electionguard_verify import verify from electionguard_verify.verify import ( Verification, verify_aggregation, verify_ballot, verify_decryption, ) __all__ = [ "Verification", "verify", "verify_aggregation", "verify_ballot", "verify_decryption", ] # </AUTOGEN_INIT> # single source version from pyproject.toml try: __version__ = importlib.metadata.version(__package__.split("_", maxsplit=1)[0]) except importlib.metadata.PackageNotFoundError: __version__ = "0.0.0" ================================================ FILE: src/electionguard_verify/verify.py ================================================ from dataclasses import dataclass from typing import Dict, Optional, List from electionguard.ballot import CiphertextBallot, SubmittedBallot from electionguard.election import CiphertextElectionContext from electionguard.key_ceremony import ElectionPublicKey from electionguard.manifest import ( InternalManifest, Manifest, ) from electionguard.type import GuardianId from electionguard.tally import PlaintextTally, CiphertextTally @dataclass class Verification: """ Representation of a verification result with an optional message """ verified: bool """Verification successful?""" message: Optional[str] def verify_ballot( ballot: CiphertextBallot, manifest: Manifest, context: CiphertextElectionContext, ) -> Verification: """ Method to verify the validity of a ballot """ if not ballot.is_valid_encryption( manifest.crypto_hash(), context.elgamal_public_key, context.crypto_extended_base_hash, ): return Verification( False, message=f"verify_ballot: mismatching ballot encryption {ballot.object_id}", ) return Verification(True, message=None) def verify_decryption( tally: PlaintextTally, election_public_keys: Dict[GuardianId, ElectionPublicKey], context: CiphertextElectionContext, ) -> Verification: for _, contest in tally.contests.items(): for selection_id, selection in contest.selections.items(): for share in selection.shares: election_public_key = election_public_keys.get(share.guardian_id).key if not share.proof.is_valid( selection.message, election_public_key, share.share, context.crypto_extended_base_hash, ): return Verification( False, message=f"verify_decryption: {selection_id} selection is not valid", ) return Verification(True, message=None) def verify_aggregation( submitted_ballots: List[SubmittedBallot], tally: CiphertextTally, manifest: Manifest, context: CiphertextElectionContext, ) -> Verification: new_tally = CiphertextTally("verify", InternalManifest(manifest), context) for ballot in submitted_ballots: new_tally.append(ballot, True) if ( isinstance(tally, CiphertextTally) and new_tally.cast_ballot_ids == tally.cast_ballot_ids and new_tally.spoiled_ballot_ids == tally.spoiled_ballot_ids and new_tally.contests == tally.contests ): return Verification(True, message=None) return Verification( False, message="verify_aggregation: aggregated value of ballots doesn't matches with tally", ) ================================================ FILE: stubs/gmpy2.pyi ================================================ """ Stub of gympy2 mpz to support typecheck. Created by running `stubgen -p gmpy2`, and then modifying the output manually Necessary to support mypy typechecking """ from typing import Union, Any, Tuple, Text, Optional class mpz(int): def __new__( self, x: Union[Text, bytes, bytearray, int], base: int = ... ) -> "mpz": ... def bit_clear(self, n: int) -> mpz: ... def bit_flip(self, n: int) -> mpz: ... def bit_length(self, *args: int, **kwargs: Any) -> int: ... def bit_scan0(self, n: int = ...) -> int: ... def bit_scan1(self, n: int = ...) -> int: ... def bit_set(self, n: int) -> mpz: ... def bit_test(self, n: int) -> bool: ... def digits(self) -> str: ... def is_divisible(self, d: int) -> bool: ... def is_even(self) -> bool: ... def is_odd(self) -> bool: ... def is_power(self) -> bool: ... def is_prime(self) -> bool: ... def is_square(self) -> bool: ... def num_digits(self, base: int = ...) -> int: ... def __abs__(self) -> mpz: ... def __add__(self, other: int) -> mpz: ... def __and__(self, other: int) -> mpz: ... def __bool__(self) -> bool: ... def __ceil__(self) -> mpz: ... def __divmod__(self, other: int) -> Tuple[int, int]: ... def __eq__(self, other: object) -> bool: ... def __float__(self) -> mpz: ... # maybe not mpz? def __floor__(self) -> mpz: ... def __floordiv__(self, other: int) -> mpz: ... def __format__(self, *args: Any, **kwargs: Any) -> str: ... def __ge__(self, other: int) -> bool: ... def __getitem__(self, index: int) -> mpz: ... def __gt__(self, other: int) -> bool: ... def __hash__(self) -> int: ... def __iadd__(self, other: int) -> mpz: ... def __ifloordiv__(self, other: int) -> mpz: ... def __ilshift__(self, other: int) -> mpz: ... def __imod__(self, other: int) -> mpz: ... def __imul__(self, other: int) -> mpz: ... def __index__(self) -> int: ... def __int__(self) -> int: ... def __invert__(self) -> mpz: ... def __irshift__(self, other: int) -> mpz: ... def __isub__(self, other: int) -> mpz: ... def __le__(self, other: int) -> bool: ... def __len__(self) -> int: ... def __lshift__(self, other: int) -> mpz: ... def __lt__(self, other: int) -> bool: ... def __mod__(self, other: int) -> mpz: ... def __mul__(self, other: int) -> mpz: ... def __ne__(self, other: object) -> bool: ... def __neg__(self) -> mpz: ... def __or__(self, other: int) -> mpz: ... def __pos__(self) -> bool: ... def __pow__(self, other: int, __modulo: Optional[int] = ...) -> Any: ... # type: ignore[override] def __radd__(self, other: int) -> mpz: ... def __rand__(self, other: int) -> mpz: ... def __rdivmod__(self, other: int) -> Tuple[int, int]: ... def __rfloordiv__(self, other: int) -> mpz: ... def __rlshift__(self, other: int) -> mpz: ... def __rmod__(self, other: int) -> mpz: ... def __rmul__(self, other: int) -> mpz: ... def __ror__(self, other: int) -> mpz: ... def __rpow__(self, other: int, __modulo: Optional[int] = ...) -> Any: ... # type: ignore[misc] def __rrshift__(self, other: int) -> mpz: ... def __rshift__(self, other: int) -> mpz: ... def __rsub__(self, other: int) -> mpz: ... def __rtruediv__(self, other: float) -> float: ... def __rxor__(self, other: int) -> mpz: ... def __sizeof__(self) -> int: ... def __sub__(self, other: int) -> mpz: ... def __truediv__(self, other: float) -> float: ... def __trunc__(self) -> mpz: ... def __xor__(self, other: int) -> mpz: ... def invert(x: mpz, m: mpz) -> mpz: ... def powmod(a: int, e: int, p: int) -> mpz: ... def to_binary(a: mpz) -> bytes: ... def from_binary(b: bytes) -> mpz: ... ================================================ FILE: tests/__init__.py ================================================ ================================================ FILE: tests/base_test_case.py ================================================ import os from unittest import TestCase from unittest.mock import patch from pytest import fixture from pytest_mock import MockerFixture from electionguard.constants import PrimeOption class BaseTestCase(TestCase): """Base Test Case for overriding environment variables.""" mocker: MockerFixture # pylint: disable=unused-private-member @fixture(autouse=True) def __inject_fixtures(self, mocker): self.mocker = mocker @classmethod def setUpClass(cls): """Set up class.""" cls.env_patcher = patch.dict( os.environ, {"PRIME_OPTION": PrimeOption.TestOnly.value} ) cls.env_patcher.start() super().setUpClass() @classmethod def tearDownClass(cls): """Tear down class.""" super().tearDownClass() cls.env_patcher.stop() ================================================ FILE: tests/bench/__init__.py ================================================ ================================================ FILE: tests/bench/bench_chaum_pedersen.py ================================================ from dataclasses import dataclass from timeit import default_timer as timer from typing import Dict, List, Tuple from statistics import mean, stdev from electionguard.chaum_pedersen import make_disjunctive_chaum_pedersen_zero from electionguard.elgamal import ( elgamal_keypair_from_secret, ElGamalKeyPair, elgamal_encrypt, ) from electionguard.group import ElementModQ, ONE_MOD_Q from electionguard.nonces import Nonces from electionguard.scheduler import Scheduler from electionguard.utils import get_optional @dataclass class BenchInput: """Input for benchmark""" keypair: ElGamalKeyPair r: ElementModQ s: ElementModQ def chaum_pedersen_bench(bi: BenchInput) -> Tuple[float, float]: """ Given an input (instance of the BenchInput tuple), constructs and validates a disjunctive Chaum-Pedersen proof, returning the time (in seconds) to do each operation. """ ciphertext = get_optional(elgamal_encrypt(0, bi.r, bi.keypair.public_key)) start1 = timer() proof = make_disjunctive_chaum_pedersen_zero( ciphertext, bi.r, bi.keypair.public_key, ONE_MOD_Q, bi.s ) end1 = timer() valid = proof.is_valid(ciphertext, bi.keypair.public_key, ONE_MOD_Q) end2 = timer() if not valid: raise Exception("Wasn't expecting an invalid proof during a benchmark!") return end1 - start1, end2 - end1 def identity(x: int) -> int: """Placeholder function used just to warm up the parallel mapper prior to benchmarking.""" return x if __name__ == "__main__": problem_sizes = (100, 500, 1000, 5000) rands = Nonces(ElementModQ(31337)) speedup: Dict[int, float] = {} # warm up the pool to help get consistent measurements with Scheduler() as scheduler: results: List[int] = scheduler.schedule( identity, [list([x]) for x in range(1, 30000)] ) assert results == list(range(1, 30000)) bench_start = timer() for size in problem_sizes: print("Benchmarking on problem size: ", size) seeds = rands[0:size] inputs = [ BenchInput( get_optional(elgamal_keypair_from_secret(a)), rands[size], rands[size + 1], ) for a in seeds ] start_all_scalar = timer() timing_data = [chaum_pedersen_bench(i) for i in inputs] end_all_scalar = timer() print(f" Creating Chaum-Pedersen proofs ({size} iterations)") avg_proof_scalar = mean([t[0] for t in timing_data]) std_proof_scalar = stdev([t[0] for t in timing_data]) print(f" Avg = {avg_proof_scalar:.6f} sec") print(f" Stddev = {std_proof_scalar:.6f} sec") print(f" Validating Chaum-Pedersen proofs ({size} iterations)") avg_verify_scalar = mean([t[1] for t in timing_data]) std_verify_scalar = stdev([t[1] for t in timing_data]) print(f" Avg = {avg_verify_scalar:.6f} sec") print(f" Stddev = {std_verify_scalar:.6f} sec") # Run in parallel start_all_parallel = timer() timing_data_parallel: List[Tuple[float, float]] = scheduler.schedule( chaum_pedersen_bench, [list([input]) for input in inputs] ) end_all_parallel = timer() speedup[size] = (end_all_scalar - start_all_scalar) / ( end_all_parallel - start_all_parallel ) print(f" Parallel speedup: {speedup[size]:.3f}x") print() print("PARALLELISM SPEEDUPS") print("Size / Speedup") for size in problem_sizes: print(f"{size:4d} / {speedup[size]:.3f}x") bench_end = timer() print() print(f"Total benchmark runtime: {bench_end - bench_start} sec") ############################################################################################################## # Performance conclusions (Dan Wallach, 21 March 2020): # On my MacPro (Xeon 6-core with hyperthreading, 3.5GHz, Python 3.8), this benchmark runs in roughly 5 minutes # and reports that C-P proofs take 10-11ms to compute and 23-24ms to verify. The parallelism numbers are: # Size / Speedup # 100 / 5.749x # 500 / 5.765x # 1000 / 5.507x # 5000 / 5.548x ================================================ FILE: tests/integration/__init__.py ================================================ ================================================ FILE: tests/integration/test_create_schema.py ================================================ import json from unittest import TestCase from shutil import rmtree from electionguard.constants import ElectionConstants from electionguard.election import CiphertextElectionContext from electionguard.guardian import GuardianRecord from electionguard.manifest import Manifest from electionguard.ballot import ( CiphertextBallot, PlaintextBallot, SubmittedBallot, ) from electionguard.ballot_compact import CompactPlaintextBallot, CompactSubmittedBallot from electionguard.election_polynomial import LagrangeCoefficientsRecord from electionguard.encrypt import EncryptionDevice from electionguard.tally import ( PublishedCiphertextTally, PlaintextTally, ) from electionguard.serialize import construct_path, get_schema, to_file class TestCreateSchema(TestCase): """Test creating schema.""" schema_dir = "schemas" remove_schema = False # TODO Fix Pydantic errors with json schema resolve_pydantic_errors = False def test_create_schema(self) -> None: to_file( json.loads((get_schema(CiphertextElectionContext))), construct_path("context_schema"), self.schema_dir, ) to_file( json.loads(get_schema(ElectionConstants)), construct_path("constants_schema"), self.schema_dir, ) to_file( json.loads(get_schema(EncryptionDevice)), construct_path("device_schema"), self.schema_dir, ) to_file( json.loads(get_schema(LagrangeCoefficientsRecord)), construct_path("coefficients_schema"), self.schema_dir, ) if self.resolve_pydantic_errors: to_file( json.loads(get_schema(Manifest)), construct_path("manifest_schema"), self.schema_dir, ) to_file( json.loads(get_schema(GuardianRecord)), construct_path("guardian_schema"), self.schema_dir, ) to_file( json.loads(get_schema(PlaintextBallot)), construct_path("plaintext_ballot_schema"), self.schema_dir, ) to_file( json.loads(get_schema(CiphertextBallot)), construct_path("ciphertext_ballot_schema"), self.schema_dir, ) to_file( json.loads(get_schema(SubmittedBallot)), construct_path("submitted_ballot_schema"), self.schema_dir, ) to_file( json.loads(get_schema(CompactPlaintextBallot)), construct_path("compact_plaintext_ballot_schema"), self.schema_dir, ) to_file( json.loads(get_schema(CompactSubmittedBallot)), construct_path("compact_submitted_ballot_schema"), self.schema_dir, ) to_file( json.loads(get_schema(PlaintextTally)), construct_path("plaintext_tally_schema"), self.schema_dir, ) to_file( json.loads(get_schema(PublishedCiphertextTally)), construct_path("ciphertext_tally_schema"), self.schema_dir, ) if self.remove_schema: rmtree(self.schema_dir) ================================================ FILE: tests/integration/test_end_to_end_election.py ================================================ #!/usr/bin/env python from typing import Callable, Dict, List, Union from os import path, remove from shutil import rmtree, make_archive from random import randint from dataclasses import asdict from tests.base_test_case import BaseTestCase from electionguard.type import BallotId from electionguard.utils import get_optional # Step 0 - Configure Election from electionguard.constants import ElectionConstants, get_constants from electionguard.election import CiphertextElectionContext from electionguard.manifest import Manifest, InternalManifest # Step 1 - Key Ceremony from electionguard.guardian import Guardian, GuardianRecord, PrivateGuardianRecord from electionguard.key_ceremony_mediator import KeyCeremonyMediator # Step 2 - Encrypt Votes from electionguard.ballot import ( BallotBoxState, CiphertextBallot, PlaintextBallot, SubmittedBallot, ) from electionguard.encrypt import EncryptionDevice from electionguard.encrypt import EncryptionMediator # Step 3 - Cast and Spoil from electionguard.data_store import DataStore from electionguard.ballot_box import BallotBox, get_ballots # Step 4 - Decrypt Tally from electionguard.tally import ( PublishedCiphertextTally, tally_ballots, CiphertextTally, PlaintextTally, ) from electionguard.decryption_mediator import DecryptionMediator from electionguard.election_polynomial import LagrangeCoefficientsRecord # Step 5 - Publish and Verify from electionguard.serialize import from_file, construct_path from electionguard_tools.helpers.export import ( COEFFICIENTS_FILE_NAME, DEVICES_DIR, GUARDIANS_DIR, PRIVATE_DATA_DIR, SPOILED_BALLOTS_DIR, SUBMITTED_BALLOTS_DIR, ELECTION_RECORD_DIR, SUBMITTED_BALLOT_PREFIX, SPOILED_BALLOT_PREFIX, CONSTANTS_FILE_NAME, CONTEXT_FILE_NAME, DEVICE_PREFIX, ENCRYPTED_TALLY_FILE_NAME, GUARDIAN_PREFIX, MANIFEST_FILE_NAME, TALLY_FILE_NAME, export_private_data, export_record, ) from electionguard_tools.factories.ballot_factory import BallotFactory from electionguard_tools.factories.election_factory import ( ElectionFactory, NUMBER_OF_GUARDIANS, ) from electionguard_tools.helpers.election_builder import ElectionBuilder devices_directory = path.join(ELECTION_RECORD_DIR, DEVICES_DIR) guardians_directory = path.join(ELECTION_RECORD_DIR, GUARDIANS_DIR) submitted_ballots_directory = path.join(ELECTION_RECORD_DIR, SUBMITTED_BALLOTS_DIR) spoiled_ballots_directory = path.join(ELECTION_RECORD_DIR, SPOILED_BALLOTS_DIR) # pylint: disable=too-many-instance-attributes class TestEndToEndElection(BaseTestCase): """ Test a complete simple example of executing an End-to-End encrypted election. In a real world scenario all of these steps would not be completed on the same machine. """ NUMBER_OF_GUARDIANS = 5 QUORUM = 3 REMOVE_RAW_OUTPUT = True REMOVE_ZIP_OUTPUT = True # Step 0 - Configure Election manifest: Manifest election_builder: ElectionBuilder internal_manifest: InternalManifest context: CiphertextElectionContext constants: ElectionConstants # Step 1 - Key Ceremony mediator: KeyCeremonyMediator guardians: List[Guardian] = [] # Step 2 - Encrypt Votes device: EncryptionDevice encrypter: EncryptionMediator plaintext_ballots: List[PlaintextBallot] ciphertext_ballots: List[CiphertextBallot] = [] # Step 3 - Cast and Spoil ballot_store: DataStore[BallotId, SubmittedBallot] ballot_box: BallotBox submitted_ballots: Dict[BallotId, SubmittedBallot] # Step 4 - Decrypt Tally ciphertext_tally: CiphertextTally plaintext_tally: PlaintextTally plaintext_spoiled_ballots: Dict[str, PlaintextTally] decryption_mediator: DecryptionMediator lagrange_coefficients: LagrangeCoefficientsRecord # Step 5 - Publish guardian_records: List[GuardianRecord] = [] private_guardian_records: List[PrivateGuardianRecord] = [] def test_end_to_end_election(self) -> None: """ Execute the simplified end-to-end test demonstrating each component of the system. """ self.step_0_configure_election() self.step_1_key_ceremony() self.step_2_encrypt_votes() self.step_3_cast_and_spoil() self.step_4_decrypt_tally() self.step_5_publish() def step_0_configure_election(self) -> None: """ To conduct an election, load an `Manifest` file. """ # Load a pre-configured Election Description # TODO: replace with complex election self.manifest = ElectionFactory().get_simple_manifest_from_file() print( f""" {'-'*40}\n # Election Summary: # Scope: {self.manifest.election_scope_id} # Geopolitical Units: {len(self.manifest.geopolitical_units)} # Parties: {len(self.manifest.parties)} # Candidates: {len(self.manifest.candidates)} # Contests: {len(self.manifest.contests)} # Ballot Styles: {len(self.manifest.ballot_styles)}\n {'-'*40}\n """ ) self._assert_message( Manifest.is_valid.__qualname__, "Verify that the input election meta-data is well-formed", self.manifest.is_valid(), ) # Create an Election Builder self.election_builder = ElectionBuilder( self.NUMBER_OF_GUARDIANS, self.QUORUM, self.manifest ) self._assert_message( ElectionBuilder.__qualname__, f"Created with number_of_guardians: {self.NUMBER_OF_GUARDIANS} quorum: {self.QUORUM}", ) # Move on to the Key Ceremony def step_1_key_ceremony(self) -> None: """ Using the NUMBER_OF_GUARDIANS, generate public-private keypairs and share representations of those keys with QUORUM of other Guardians. Then, combine the public election keys to make a joint election key that is used to encrypt ballots. """ # Setup Guardians for i in range(self.NUMBER_OF_GUARDIANS): self.guardians.append( Guardian.from_nonce( str(i + 1), i + 1, self.NUMBER_OF_GUARDIANS, self.QUORUM, ) ) # Setup Mediator self.mediator = KeyCeremonyMediator( "mediator_1", self.guardians[0].ceremony_details ) # ROUND 1: Public Key Sharing # Announce for guardian in self.guardians: self.mediator.announce(guardian.share_key()) # Share Keys for guardian in self.guardians: announced_keys = get_optional(self.mediator.share_announced()) for key in announced_keys: if guardian.id is not key.owner_id: guardian.save_guardian_key(key) self._assert_message( KeyCeremonyMediator.all_guardians_announced.__qualname__, "Confirms all guardians have shared their public keys", self.mediator.all_guardians_announced(), ) # ROUND 2: Election Partial Key Backup Sharing # Share Backups for sending_guardian in self.guardians: sending_guardian.generate_election_partial_key_backups() backups = [] for designated_guardian in self.guardians: if designated_guardian.id != sending_guardian.id: backups.append( get_optional( sending_guardian.share_election_partial_key_backup( designated_guardian.id ) ) ) self.mediator.receive_backups(backups) self._assert_message( KeyCeremonyMediator.receive_backups.__qualname__, "Receive election partial key backups from key owning guardian", len(backups) == NUMBER_OF_GUARDIANS - 1, ) self._assert_message( KeyCeremonyMediator.all_backups_available.__qualname__, "Confirm all guardians have shared their election partial key backups", self.mediator.all_backups_available(), ) # Receive Backups for designated_guardian in self.guardians: backups = get_optional(self.mediator.share_backups(designated_guardian.id)) self._assert_message( KeyCeremonyMediator.share_backups.__qualname__, "Share election partial key backups for the designated guardian", len(backups) == NUMBER_OF_GUARDIANS - 1, ) for backup in backups: designated_guardian.save_election_partial_key_backup(backup) # ROUND 3: Verification of Backups # Verify Backups for designated_guardian in self.guardians: verifications = [] for backup_owner in self.guardians: if designated_guardian.id is not backup_owner.id: verification = ( designated_guardian.verify_election_partial_key_backup( backup_owner.id ) ) verifications.append(get_optional(verification)) self.mediator.receive_backup_verifications(verifications) self._assert_message( KeyCeremonyMediator.all_backups_verified.__qualname__, "Confirms all guardians have verified the backups of all other guardians", self.mediator.all_backups_verified(), ) # FINAL: Publish Joint Key joint_key = self.mediator.publish_joint_key() self._assert_message( KeyCeremonyMediator.publish_joint_key.__qualname__, "Publishes the Joint Election Key", joint_key is not None, ) # Build the Election self.election_builder.set_public_key(get_optional(joint_key).joint_public_key) self.election_builder.set_commitment_hash( get_optional(joint_key).commitment_hash ) self.internal_manifest, self.context = get_optional( self.election_builder.build() ) self.constants = get_constants() # Move on to encrypting ballots def step_2_encrypt_votes(self) -> None: """ Using the `CiphertextElectionContext` encrypt ballots for the election. """ # Configure the Encryption Device self.device = ElectionFactory.get_encryption_device() self.encrypter = EncryptionMediator( self.internal_manifest, self.context, self.device ) self._assert_message( EncryptionDevice.__qualname__, f"Ready to encrypt at location: {self.device.location}", ) # Load some Ballots self.plaintext_ballots = BallotFactory().get_simple_ballots_from_file() self._assert_message( PlaintextBallot.__qualname__, f"Loaded ballots: {len(self.plaintext_ballots)}", len(self.plaintext_ballots) > 0, ) # Encrypt the Ballots for plaintext_ballot in self.plaintext_ballots: encrypted_ballot = self.encrypter.encrypt(plaintext_ballot) self._assert_message( EncryptionMediator.encrypt.__qualname__, f"Ballot Id: {plaintext_ballot.object_id}", encrypted_ballot is not None, ) self.ciphertext_ballots.append(get_optional(encrypted_ballot)) # Next, we cast or spoil the ballots def step_3_cast_and_spoil(self) -> None: """ Accept each ballot by marking it as either cast or spoiled. This example demonstrates one way to accept ballots using the `BallotBox` class. """ # Configure the Ballot Box self.ballot_store = DataStore() self.ballot_box = BallotBox( self.internal_manifest, self.context, self.ballot_store ) # Randomly cast or spoil the ballots for ballot in self.ciphertext_ballots: if randint(0, 1): submitted_ballot = self.ballot_box.cast(ballot) else: submitted_ballot = self.ballot_box.spoil(ballot) self._assert_message( BallotBox.__qualname__, f"Submitted Ballot Id: {ballot.object_id} state: {get_optional(submitted_ballot).state}", submitted_ballot is not None, ) def step_4_decrypt_tally(self) -> None: """ Homomorphically combine the selections made on all of the cast ballots and use the Available Guardians to decrypt the combined tally. In this way, no individual voter's cast ballot is ever decrypted drectly. """ # Generate a Homomorphically Accumulated Tally of the ballots self.ciphertext_tally = get_optional( tally_ballots(self.ballot_store, self.internal_manifest, self.context) ) self.submitted_ballots = get_ballots(self.ballot_store, BallotBoxState.SPOILED) self._assert_message( tally_ballots.__qualname__, f""" - cast: {self.ciphertext_tally.cast()} - spoiled: {self.ciphertext_tally.spoiled()} Total: {len(self.ciphertext_tally)} """, self.ciphertext_tally is not None, ) # Configure the Decryption submitted_ballots_list = list(self.submitted_ballots.values()) self.decryption_mediator = DecryptionMediator( "decryption-mediator", self.context, ) # Announce each guardian as present count = 0 for guardian in self.guardians: guardian_key = guardian.share_key() tally_share = guardian.compute_tally_share( self.ciphertext_tally, self.context ) ballot_shares = guardian.compute_ballot_shares( submitted_ballots_list, self.context ) self.decryption_mediator.announce( guardian_key, get_optional(tally_share), ballot_shares ) count += 1 self._assert_message( DecryptionMediator.announce.__qualname__, f"Guardian Present: {guardian.id}", len(self.decryption_mediator.get_available_guardians()) == count, ) self.lagrange_coefficients = LagrangeCoefficientsRecord( self.decryption_mediator.get_lagrange_coefficients() ) # Get the plaintext Tally self.plaintext_tally = get_optional( self.decryption_mediator.get_plaintext_tally( self.ciphertext_tally, self.manifest ) ) self._assert_message( DecryptionMediator.get_plaintext_tally.__qualname__, "Tally Decrypted", self.plaintext_tally is not None, ) # Get the plaintext Spoiled Ballots self.plaintext_spoiled_ballots = get_optional( self.decryption_mediator.get_plaintext_ballots( submitted_ballots_list, self.manifest ) ) self._assert_message( DecryptionMediator.get_plaintext_ballots.__qualname__, "Spoiled Ballots Decrypted", self.plaintext_tally is not None, ) # Now, compare the results self.compare_results() def compare_results(self) -> None: """ Compare the results to ensure the decryption was done correctly. """ print( f""" {'-'*40} # Election Results: """ ) # Create a representation of each contest's tally selection_ids = [ selection.object_id for contest in self.manifest.contests for selection in contest.ballot_selections ] expected_plaintext_tally: Dict[str, int] = {key: 0 for key in selection_ids} # Tally the expected values from the loaded ballots for ballot in self.plaintext_ballots: if ( get_optional(self.ballot_store.get(ballot.object_id)).state == BallotBoxState.CAST ): for contest in ballot.contests: for selection in contest.ballot_selections: expected_plaintext_tally[selection.object_id] += selection.vote # Compare the expected tally to the decrypted tally for tally_contest in self.plaintext_tally.contests.values(): print(f" Contest: {tally_contest.object_id}") for tally_selection in tally_contest.selections.values(): expected = expected_plaintext_tally[tally_selection.object_id] self._assert_message( f" - Selection: {tally_selection.object_id}", f"expected: {expected}, actual: {tally_selection.tally}", expected == tally_selection.tally, ) print(f"\n{'-'*40}\n") # Compare the expected values for each spoiled ballot for ballot in self.plaintext_ballots: if ( get_optional(self.ballot_store.get(ballot.object_id)).state == BallotBoxState.SPOILED ): print(f"\nSpoiled Ballot: {ballot.object_id}") for contest in ballot.contests: print(f"\n Contest: {contest.object_id}") for selection in contest.ballot_selections: expected = selection.vote decrypted_selection = ( self.plaintext_spoiled_ballots[ballot.object_id] .contests[contest.object_id] .selections[selection.object_id] ) self._assert_message( f" - Selection: {selection.object_id}", f"expected: {expected}, actual: {decrypted_selection.tally}", expected == decrypted_selection.tally, ) def step_5_publish(self) -> None: """Publish results/artifacts of the election.""" self.guardian_records = [guardian.publish() for guardian in self.guardians] self.private_guardian_records = [ guardian.export_private_data() for guardian in self.guardians ] export_record( self.manifest, self.context, self.constants, [self.device], self.ballot_store.all(), self.plaintext_spoiled_ballots.values(), self.ciphertext_tally.publish(), self.plaintext_tally, self.guardian_records, self.lagrange_coefficients, ) self._assert_message( "Publish", f"Election Record published to: {ELECTION_RECORD_DIR}", path.exists(ELECTION_RECORD_DIR), ) export_private_data( self.plaintext_ballots, self.ciphertext_ballots, self.private_guardian_records, ) self._assert_message( "Export private data", f"Private data exported to: {PRIVATE_DATA_DIR}", path.exists(PRIVATE_DATA_DIR), ) ZIP_SUFFIX = "zip" make_archive(ELECTION_RECORD_DIR, ZIP_SUFFIX, ELECTION_RECORD_DIR) make_archive(PRIVATE_DATA_DIR, ZIP_SUFFIX, PRIVATE_DATA_DIR) self.deserialize_data() if self.REMOVE_RAW_OUTPUT: rmtree(ELECTION_RECORD_DIR) rmtree(PRIVATE_DATA_DIR) if self.REMOVE_ZIP_OUTPUT: remove(f"{ELECTION_RECORD_DIR}.{ZIP_SUFFIX}") remove(f"{PRIVATE_DATA_DIR}.{ZIP_SUFFIX}") def deserialize_data(self) -> None: """Ensure published data can be deserialized.""" # Deserialize manifest_from_file = from_file( Manifest, construct_path(MANIFEST_FILE_NAME, ELECTION_RECORD_DIR), ) self.assertEqualAsDicts(self.manifest, manifest_from_file) context_from_file = from_file( CiphertextElectionContext, construct_path(CONTEXT_FILE_NAME, ELECTION_RECORD_DIR), ) self.assertEqualAsDicts(self.context, context_from_file) constants_from_file = from_file( ElectionConstants, construct_path(CONSTANTS_FILE_NAME, ELECTION_RECORD_DIR) ) self.assertEqualAsDicts(self.constants, constants_from_file) coefficients_from_file = from_file( LagrangeCoefficientsRecord, construct_path(COEFFICIENTS_FILE_NAME, ELECTION_RECORD_DIR), ) self.assertEqualAsDicts(self.lagrange_coefficients, coefficients_from_file) device_from_file = from_file( EncryptionDevice, construct_path( DEVICE_PREFIX + str(self.device.device_id), devices_directory ), ) self.assertEqualAsDicts(self.device, device_from_file) for ballot in self.ballot_store.all(): ballot_from_file = from_file( SubmittedBallot, construct_path( SUBMITTED_BALLOT_PREFIX + ballot.object_id, submitted_ballots_directory, ), ) self.assertTrue( ballot_from_file.is_valid_encryption( self.internal_manifest.manifest_hash, self.context.elgamal_public_key, self.context.crypto_extended_base_hash, ) ) self.assertEqualAsDicts(ballot, ballot_from_file) for spoiled_ballot in self.plaintext_spoiled_ballots.values(): spoiled_ballot_from_file = from_file( PlaintextTally, construct_path( SPOILED_BALLOT_PREFIX + spoiled_ballot.object_id, spoiled_ballots_directory, ), ) self.assertEqualAsDicts(spoiled_ballot, spoiled_ballot_from_file) published_ciphertext_tally_from_file = from_file( PublishedCiphertextTally, construct_path(ENCRYPTED_TALLY_FILE_NAME, ELECTION_RECORD_DIR), ) self.assertEqualAsDicts( self.ciphertext_tally.publish(), published_ciphertext_tally_from_file, ) plainttext_tally_from_file = from_file( PlaintextTally, construct_path(TALLY_FILE_NAME, ELECTION_RECORD_DIR) ) self.assertEqualAsDicts(self.plaintext_tally, plainttext_tally_from_file) for guardian_record in self.guardian_records: guardian_record_from_file = from_file( GuardianRecord, construct_path( GUARDIAN_PREFIX + guardian_record.guardian_id, guardians_directory ), ) self.assertEqualAsDicts(guardian_record, guardian_record_from_file) def _assert_message( self, name: str, message: str, condition: Union[Callable, bool] = True ) -> None: if callable(condition): result = condition() else: result = condition print(f"{name}: {message}: {result}") self.assertTrue(result) def assertEqualAsDicts(self, first: object, second: object) -> None: """ Specialty assertEqual to compare dataclasses as dictionaries. This is relevant specifically to using pydantic dataclasses to import. Pydantic reconstructs dataclasses with name uniqueness to add their validation. This creates a naming issue where the default equality check fails. """ self.assertEqual(asdict(first), asdict(second)) if __name__ == "__main__": print("Welcome to the ElectionGuard end-to-end test") TestEndToEndElection().test_end_to_end_election() ================================================ FILE: tests/integration/test_functional_key_ceremony.py ================================================ from typing import Dict, List from tests.base_test_case import BaseTestCase from electionguard.key_ceremony import ( ElectionKeyPair, ElectionPartialKeyBackup, ElectionPartialKeyVerification, ElectionPublicKey, combine_election_public_keys, generate_election_key_pair, generate_election_partial_key_backup, generate_election_partial_key_challenge, verify_election_partial_key_backup, verify_election_partial_key_challenge, ) from electionguard.type import GuardianId class TestKeyCeremony(BaseTestCase): """ Test the key ceremony entirely from a functional sense This demonstrates that no stateful models are required and allows users to see the full flow utilizing only the core methods. """ # Basic Types SENDER_ID = GuardianId RECEIVER_ID = GuardianId # Key Ceremony Inputs NUMBER_OF_GUARDIANS = 5 QUORUM = 3 # ROUND 1 election_key_pairs: Dict[str, ElectionKeyPair] = {} guardian_keys: Dict[str, ElectionPublicKey] = {} # ROUND 2 sent_backups: Dict[SENDER_ID, List[ElectionPartialKeyBackup]] = {} received_backups: Dict[RECEIVER_ID, List[ElectionPartialKeyBackup]] = {} # ROUND 3 sent_verifications: Dict[SENDER_ID, List[ElectionPartialKeyVerification]] = {} received_verifications: Dict[RECEIVER_ID, List[ElectionPartialKeyVerification]] = {} def test_key_ceremony(self) -> None: # Determine guardian id and sequence order guardian_sequence_orders = [*range(1, self.NUMBER_OF_GUARDIANS + 1)] guardian_ids = [f"guardian_{i}" for i in guardian_sequence_orders] # ROUND 1 - SHARE KEYS # Each guardian generates their own keys for guardian_id, sequence_order in zip(guardian_ids, guardian_sequence_orders): self._guardian_generates_keys(guardian_id, sequence_order) self.assertEqual(len(self.election_key_pairs), self.NUMBER_OF_GUARDIANS) # Each guardian shares their keys for guardian_id in guardian_ids: self._guardian_share_keys(guardian_id) self.assertEqual(len(self.guardian_keys), self.NUMBER_OF_GUARDIANS) # ROUND 2 - SHARE BACKUPS # Each guardian generates and shares their backups for guardian_id in guardian_ids: self._guardian_generates_backups(guardian_id) self._guardian_shares_backups(guardian_id) # ROUND 3 - VERIFY BACKUPS # Each guardian verifies the backups they received # then shares each of the verifications to the owner of the key for guardian_id in guardian_ids: self._guardian_verifies_backups(guardian_id) self._guardian_shares_verifications(guardian_id) # ROUND 4 - CHALLENGES (IF NECESSARY) # If a verification fails, the key owner can challenge # and request another guardian verify the backup was indeed valid # In this example, we make a fake failure. self._guardian_challenges(guardian_ids) # CREATE JOINT KEY # If all backups are verified, publish joint key self._publish_joint_key() def _guardian_generates_keys( self, guardian_id: GuardianId, sequence_order: int ) -> None: """Guardian generates their keys""" # Create Election Key Pair election_key_pair = generate_election_key_pair( guardian_id, sequence_order, self.QUORUM ) self.assertIsNotNone(election_key_pair) self.election_key_pairs[guardian_id] = election_key_pair def _guardian_share_keys(self, guardian_id: GuardianId) -> None: """Guardian shares public keys""" election_key_pair = self.election_key_pairs[guardian_id] key = election_key_pair.share() self.assertIsNotNone(key) self.guardian_keys[guardian_id] = key def _guardian_generates_backups(self, sender_id: str) -> None: """ Guardian generates a partial key backup of all other guardian keys """ backups = [] for recipient_guardian in self.guardian_keys.values(): # Guardian skips themselves if recipient_guardian.owner_id is sender_id: continue senders_polynomial = self.election_key_pairs[sender_id].polynomial backup = generate_election_partial_key_backup( sender_id, senders_polynomial, recipient_guardian, ) backups.append(backup) self.sent_backups[sender_id] = backups def _guardian_shares_backups(self, sender_id: GuardianId) -> None: """Mock round robin to demonstrate sharing of backups""" backups_to_send = self.sent_backups[sender_id] for backup in backups_to_send: received_backups = self.received_backups.get(backup.designated_id) if received_backups is None: received_backups = [] received_backups.append(backup) self.received_backups[backup.designated_id] = received_backups def _guardian_verifies_backups(self, verifier_id: str) -> None: """Guardian verifies the backups they have received""" verifications = [] for backup in self.received_backups[verifier_id]: owner_public_key = self.election_key_pairs[backup.owner_id].share() verification = verify_election_partial_key_backup( verifier_id, backup, owner_public_key, self.election_key_pairs[verifier_id], ) verifications.append(verification) self.sent_verifications[verifier_id] = verifications def _guardian_shares_verifications(self, verifier_id: str) -> None: """Mock round robin to demonstrate sharing of verifications""" verifications_to_send = self.sent_verifications[verifier_id] for verification in verifications_to_send: received_verifications = self.received_verifications.get( verification.owner_id ) if received_verifications is None: received_verifications = [] received_verifications.append(verification) self.received_verifications[verification.owner_id] = received_verifications def _guardian_checks_returned_verifications(self, key_owner_id: GuardianId) -> None: """ Guardian checks that all backups have been verified sucessfully """ verifications = self.received_verifications[key_owner_id] for verification in verifications: self.assertTrue(verification.verified) def _guardian_challenges(self, guardian_ids: List[GuardianId]) -> None: key_owner_id = guardian_ids[0] original_verification = self.received_verifications[key_owner_id][0] failed_verification = ElectionPartialKeyVerification( original_verification.owner_id, original_verification.designated_id, original_verification.verifier_id, False, ) designated_id = original_verification.designated_id # If backup failed to be verified self.assertFalse(failed_verification.verified) # Key Owner generate challenge designated_backup = [ backup for backup in self.sent_backups[key_owner_id] if backup.designated_id == designated_id ][0] self.assertIsNotNone(designated_backup) election_key_pair = self.election_key_pairs[key_owner_id] challenge = generate_election_partial_key_challenge( designated_backup, election_key_pair.polynomial ) # Get a mediator to verify who is not the owner or originally designated guardian who previously verified new_verifier_id = "mediator_id" # New verifier verifies challenge verification = verify_election_partial_key_challenge( new_verifier_id, challenge, ) self.assertIsNotNone(verification) self.assertNotEqual(key_owner_id, verification.verifier_id) self.assertNotEqual(designated_id, verification.verifier_id) self.assertTrue(verification.verified) self.received_verifications[key_owner_id][0] = verification def _publish_joint_key(self) -> None: guardian_public_keys = [ keys.share() for keys in self.election_key_pairs.values() ] election_joint_key = combine_election_public_keys(guardian_public_keys) self.assertIsNotNone(election_joint_key) ================================================ FILE: tests/integration/test_hamilton_county_election.py ================================================ from tests.base_test_case import BaseTestCase import electionguard_tools.factories.election_factory as ElectionFactory import electionguard_tools.factories.ballot_factory as BallotFactory election_factory = ElectionFactory.ElectionFactory() ballot_factory = BallotFactory.BallotFactory() class TestHamiltonCountyElection(BaseTestCase): """ Demonstrates a non-trivial example using realistic input data """ def test_manifest_is_valid(self) -> None: # Act subject = election_factory.get_hamilton_manifest_from_file() # Assert self.assertTrue(subject.is_valid()) ================================================ FILE: tests/property/__init__.py ================================================ ================================================ FILE: tests/property/test_ballot.py ================================================ from typing import Tuple from datetime import timedelta from hypothesis import HealthCheck from hypothesis import given, settings from tests.base_test_case import BaseTestCase from electionguard.ballot import PlaintextBallotSelection import electionguard_tools.factories.ballot_factory as BallotFactory class TestBallot(BaseTestCase): """Ballot tests""" def test_ballot_is_valid(self): # Arrange factory = BallotFactory.BallotFactory() # Act subject = factory.get_simple_ballot_from_file() first_contest = subject.contests[0] self.assertIsNotNone(first_contest) # Assert self.assertIsNotNone(subject.object_id) self.assertEqual(subject.object_id, "some-external-id-string-123") self.assertTrue(subject.is_valid("jefferson-county-ballot-style")) self.assertTrue(first_contest.is_valid("justice-supreme-court", 2, 2)) self.assertFalse(first_contest.is_valid("some-other-contest", 2, 2)) self.assertFalse(first_contest.is_valid("justice-supreme-court", 1, 2)) self.assertFalse(first_contest.is_valid("justice-supreme-court", 2, 1)) self.assertFalse(first_contest.is_valid("justice-supreme-court", 2, 2, 1)) @settings( deadline=timedelta(milliseconds=2000), suppress_health_check=[HealthCheck.too_slow], max_examples=10, ) @given(BallotFactory.get_selection_well_formed()) def test_plaintext_ballot_selection_is_valid( self, subject: Tuple[str, PlaintextBallotSelection] ): # Arrange object_id, selection = subject # Act as_int = selection.vote is_valid = selection.is_valid(object_id) # Assert self.assertTrue(is_valid) self.assertTrue(0 <= as_int <= 1) @settings( deadline=timedelta(milliseconds=2000), suppress_health_check=[HealthCheck.too_slow], max_examples=10, ) @given(BallotFactory.get_selection_poorly_formed()) def test_plaintext_ballot_selection_is_invalid( self, subject: Tuple[str, PlaintextBallotSelection] ): # Arrange object_id, selection = subject a_different_object_id = f"{object_id}-not-the-same" # Act as_int = selection.vote is_valid = selection.is_valid(a_different_object_id) # Assert self.assertFalse(is_valid) self.assertTrue(0 <= as_int <= 1) ================================================ FILE: tests/property/test_chaum_pedersen.py ================================================ from datetime import timedelta from hypothesis import given, settings, HealthCheck, Phase from hypothesis.strategies import integers from tests.base_test_case import BaseTestCase from electionguard.chaum_pedersen import ( ConstantChaumPedersenProof, make_disjunctive_chaum_pedersen_zero, make_disjunctive_chaum_pedersen_one, make_chaum_pedersen, make_constant_chaum_pedersen, make_disjunctive_chaum_pedersen, ) from electionguard.elgamal import ( ElGamalKeyPair, elgamal_encrypt, elgamal_keypair_from_secret, ) from electionguard.group import ElementModQ, TWO_MOD_Q, ONE_MOD_Q, int_to_p, TWO_MOD_P from electionguard.utils import get_optional from electionguard_tools.strategies.elgamal import elgamal_keypairs from electionguard_tools.strategies.group import elements_mod_q_no_zero, elements_mod_q class TestDisjunctiveChaumPedersen(BaseTestCase): """Disjunctive Chaum Pedersen tests""" def test_djcp_proofs_simple(self): # doesn't get any simpler than this keypair = elgamal_keypair_from_secret(TWO_MOD_Q) nonce = ONE_MOD_Q seed = TWO_MOD_Q message0 = get_optional(elgamal_encrypt(0, nonce, keypair.public_key)) proof0 = make_disjunctive_chaum_pedersen_zero( message0, nonce, keypair.public_key, ONE_MOD_Q, seed ) proof0bad = make_disjunctive_chaum_pedersen_one( message0, nonce, keypair.public_key, ONE_MOD_Q, seed ) self.assertTrue(proof0.is_valid(message0, keypair.public_key, ONE_MOD_Q)) self.assertFalse(proof0bad.is_valid(message0, keypair.public_key, ONE_MOD_Q)) message1 = get_optional(elgamal_encrypt(1, nonce, keypair.public_key)) proof1 = make_disjunctive_chaum_pedersen_one( message1, nonce, keypair.public_key, ONE_MOD_Q, seed ) proof1bad = make_disjunctive_chaum_pedersen_zero( message1, nonce, keypair.public_key, ONE_MOD_Q, seed ) self.assertTrue(proof1.is_valid(message1, keypair.public_key, ONE_MOD_Q)) self.assertFalse(proof1bad.is_valid(message1, keypair.public_key, ONE_MOD_Q)) def test_djcp_proof_invalid_inputs(self): # this is here to push up our coverage keypair = elgamal_keypair_from_secret(TWO_MOD_Q) nonce = ONE_MOD_Q seed = TWO_MOD_Q message0 = get_optional(elgamal_encrypt(0, nonce, keypair.public_key)) self.assertRaises( Exception, make_disjunctive_chaum_pedersen, message0, nonce, keypair.public_key, seed, 3, ) @settings( deadline=timedelta(milliseconds=2000), suppress_health_check=[HealthCheck.too_slow], max_examples=10, ) @given(elgamal_keypairs(), elements_mod_q_no_zero(), elements_mod_q()) def test_djcp_proof_zero( self, keypair: ElGamalKeyPair, nonce: ElementModQ, seed: ElementModQ ): message = get_optional(elgamal_encrypt(0, nonce, keypair.public_key)) proof = make_disjunctive_chaum_pedersen_zero( message, nonce, keypair.public_key, ONE_MOD_Q, seed ) proof_bad = make_disjunctive_chaum_pedersen_one( message, nonce, keypair.public_key, ONE_MOD_Q, seed ) self.assertTrue(proof.is_valid(message, keypair.public_key, ONE_MOD_Q)) self.assertFalse(proof_bad.is_valid(message, keypair.public_key, ONE_MOD_Q)) @settings( deadline=timedelta(milliseconds=2000), suppress_health_check=[HealthCheck.too_slow], max_examples=10, ) @given(elgamal_keypairs(), elements_mod_q_no_zero(), elements_mod_q()) def test_djcp_proof_one( self, keypair: ElGamalKeyPair, nonce: ElementModQ, seed: ElementModQ ): message = get_optional(elgamal_encrypt(1, nonce, keypair.public_key)) proof = make_disjunctive_chaum_pedersen_one( message, nonce, keypair.public_key, ONE_MOD_Q, seed ) proof_bad = make_disjunctive_chaum_pedersen_zero( message, nonce, keypair.public_key, ONE_MOD_Q, seed ) self.assertTrue(proof.is_valid(message, keypair.public_key, ONE_MOD_Q)) self.assertFalse(proof_bad.is_valid(message, keypair.public_key, ONE_MOD_Q)) @settings( deadline=timedelta(milliseconds=2000), suppress_health_check=[HealthCheck.too_slow], max_examples=10, ) @given(elgamal_keypairs(), elements_mod_q_no_zero(), elements_mod_q()) def test_djcp_proof_broken( self, keypair: ElGamalKeyPair, nonce: ElementModQ, seed: ElementModQ ): # verify two different ways to generate an invalid C-P proof. message = get_optional(elgamal_encrypt(0, nonce, keypair.public_key)) message_bad = get_optional(elgamal_encrypt(2, nonce, keypair.public_key)) proof = make_disjunctive_chaum_pedersen_zero( message, nonce, keypair.public_key, ONE_MOD_Q, seed ) proof_bad = make_disjunctive_chaum_pedersen_zero( message_bad, nonce, keypair.public_key, ONE_MOD_Q, seed ) self.assertFalse(proof_bad.is_valid(message_bad, keypair.public_key, ONE_MOD_Q)) self.assertFalse(proof.is_valid(message_bad, keypair.public_key, ONE_MOD_Q)) class TestChaumPedersen(BaseTestCase): """Chaum Pedersen tests""" def test_cp_proofs_simple(self): keypair = elgamal_keypair_from_secret(TWO_MOD_Q) nonce = ONE_MOD_Q seed = TWO_MOD_Q message = get_optional(elgamal_encrypt(0, nonce, keypair.public_key)) decryption = message.partial_decrypt(keypair.secret_key) proof = make_chaum_pedersen( message, keypair.secret_key, decryption, seed, ONE_MOD_Q ) bad_proof = make_chaum_pedersen( message, keypair.secret_key, TWO_MOD_P, seed, ONE_MOD_Q ) self.assertTrue( proof.is_valid(message, keypair.public_key, decryption, ONE_MOD_Q) ) self.assertFalse( bad_proof.is_valid(message, keypair.public_key, decryption, ONE_MOD_Q) ) @settings( deadline=timedelta(milliseconds=2000), suppress_health_check=[HealthCheck.too_slow], max_examples=10, phases=[Phase.explicit, Phase.reuse, Phase.generate, Phase.target], ) @given( elgamal_keypairs(), elements_mod_q_no_zero(), elements_mod_q(), integers(0, 100), integers(0, 100), ) def test_cp_proof( self, keypair: ElGamalKeyPair, nonce: ElementModQ, seed: ElementModQ, constant: int, bad_constant: int, ): if constant == bad_constant: bad_constant = constant + 1 message = get_optional(elgamal_encrypt(constant, nonce, keypair.public_key)) decryption = message.partial_decrypt(keypair.secret_key) proof = make_chaum_pedersen( message, keypair.secret_key, decryption, seed, ONE_MOD_Q ) bad_proof = make_chaum_pedersen( message, keypair.secret_key, int_to_p(bad_constant), seed, ONE_MOD_Q ) self.assertTrue( proof.is_valid(message, keypair.public_key, decryption, ONE_MOD_Q) ) self.assertFalse( bad_proof.is_valid(message, keypair.public_key, decryption, ONE_MOD_Q) ) class TestConstantChaumPedersen(BaseTestCase): """Constant Chaum Pedersen tests""" def test_ccp_proofs_simple_encryption_of_zero(self): keypair = elgamal_keypair_from_secret(TWO_MOD_Q) nonce = ONE_MOD_Q seed = TWO_MOD_Q message = get_optional(elgamal_encrypt(0, nonce, keypair.public_key)) proof = make_constant_chaum_pedersen( message, 0, nonce, keypair.public_key, seed, ONE_MOD_Q ) bad_proof = make_constant_chaum_pedersen( message, 1, nonce, keypair.public_key, seed, ONE_MOD_Q ) self.assertTrue(proof.is_valid(message, keypair.public_key, ONE_MOD_Q)) self.assertFalse(bad_proof.is_valid(message, keypair.public_key, ONE_MOD_Q)) def test_ccp_proofs_simple_encryption_of_one(self): keypair = elgamal_keypair_from_secret(TWO_MOD_Q) nonce = ONE_MOD_Q seed = TWO_MOD_Q message = get_optional(elgamal_encrypt(1, nonce, keypair.public_key)) proof = make_constant_chaum_pedersen( message, 1, nonce, keypair.public_key, seed, ONE_MOD_Q ) bad_proof = make_constant_chaum_pedersen( message, 0, nonce, keypair.public_key, seed, ONE_MOD_Q ) self.assertTrue(proof.is_valid(message, keypair.public_key, ONE_MOD_Q)) self.assertFalse(bad_proof.is_valid(message, keypair.public_key, ONE_MOD_Q)) @settings( deadline=timedelta(milliseconds=2000), suppress_health_check=[HealthCheck.too_slow], max_examples=10, ) @given( elgamal_keypairs(), elements_mod_q_no_zero(), elements_mod_q(), integers(0, 100), integers(0, 100), ) def test_ccp_proof( self, keypair: ElGamalKeyPair, nonce: ElementModQ, seed: ElementModQ, constant: int, bad_constant: int, ): # assume() slows down the test-case generation # so assume(constant != bad_constant) if constant == bad_constant: bad_constant = constant + 1 message = get_optional(elgamal_encrypt(constant, nonce, keypair.public_key)) message_bad = get_optional( elgamal_encrypt(bad_constant, nonce, keypair.public_key) ) proof = make_constant_chaum_pedersen( message, constant, nonce, keypair.public_key, seed, ONE_MOD_Q ) self.assertTrue(proof.is_valid(message, keypair.public_key, ONE_MOD_Q)) proof_bad1 = make_constant_chaum_pedersen( message_bad, constant, nonce, keypair.public_key, seed, ONE_MOD_Q ) self.assertFalse( proof_bad1.is_valid(message_bad, keypair.public_key, ONE_MOD_Q) ) proof_bad2 = make_constant_chaum_pedersen( message, bad_constant, nonce, keypair.public_key, seed, ONE_MOD_Q ) self.assertFalse(proof_bad2.is_valid(message, keypair.public_key, ONE_MOD_Q)) proof_bad3 = ConstantChaumPedersenProof( proof.pad, proof.data, proof.challenge, proof.response, -1 ) self.assertFalse(proof_bad3.is_valid(message, keypair.public_key, ONE_MOD_Q)) ================================================ FILE: tests/property/test_decrypt_with_secrets.py ================================================ from copy import deepcopy from datetime import timedelta from random import Random from typing import Tuple from hypothesis import HealthCheck, Phase from hypothesis import given, settings from hypothesis.strategies import integers from tests.base_test_case import BaseTestCase from electionguard.chaum_pedersen import DisjunctiveChaumPedersenProof from electionguard.decrypt_with_secrets import ( decrypt_selection_with_secret, decrypt_selection_with_nonce, decrypt_contest_with_secret, decrypt_contest_with_nonce, decrypt_ballot_with_nonce, decrypt_ballot_with_secret, ) from electionguard.elgamal import ElGamalKeyPair, ElGamalCiphertext from electionguard.encrypt import ( encrypt_contest, encrypt_selection, EncryptionMediator, ) from electionguard.group import ( ElementModQ, TWO_MOD_P, ONE_MOD_Q, mult_p, ) from electionguard.manifest import ( ContestDescription, SelectionDescription, generate_placeholder_selections_from, contest_description_with_placeholders_from, ) import electionguard_tools.factories.ballot_factory as BallotFactory import electionguard_tools.factories.election_factory as ElectionFactory from electionguard_tools.strategies.elgamal import elgamal_keypairs from electionguard_tools.strategies.group import elements_mod_q_no_zero election_factory = ElectionFactory.ElectionFactory() ballot_factory = BallotFactory.BallotFactory() class TestDecryptWithSecrets(BaseTestCase): """Decrypting with secrets tests""" @settings( deadline=timedelta(milliseconds=2000), suppress_health_check=[HealthCheck.too_slow], max_examples=10, # disabling the "shrink" phase, because it runs very slowly phases=[Phase.explicit, Phase.reuse, Phase.generate, Phase.target], ) @given( ElectionFactory.get_selection_description_well_formed(), elgamal_keypairs(), elements_mod_q_no_zero(), integers(), ) def test_decrypt_selection_valid_input_succeeds( self, selection_description: Tuple[str, SelectionDescription], keypair: ElGamalKeyPair, nonce_seed: ElementModQ, random_seed: int, ): # Arrange random = Random(random_seed) _, description = selection_description data = ballot_factory.get_random_selection_from(description, random) # Act subject = encrypt_selection( data, description, keypair.public_key, ONE_MOD_Q, nonce_seed, should_verify_proofs=True, ) self.assertIsNotNone(subject) result_from_key = decrypt_selection_with_secret( subject, description, keypair.public_key, keypair.secret_key, ONE_MOD_Q ) result_from_nonce = decrypt_selection_with_nonce( subject, description, keypair.public_key, ONE_MOD_Q ) result_from_nonce_seed = decrypt_selection_with_nonce( subject, description, keypair.public_key, ONE_MOD_Q, nonce_seed ) # Assert self.assertIsNotNone(result_from_key) self.assertIsNotNone(result_from_nonce) self.assertIsNotNone(result_from_nonce_seed) self.assertEqual(data.vote, result_from_key.vote) self.assertEqual(data.vote, result_from_nonce.vote) self.assertEqual(data.vote, result_from_nonce_seed.vote) @settings( deadline=timedelta(milliseconds=2000), suppress_health_check=[HealthCheck.too_slow], max_examples=10, # disabling the "shrink" phase, because it runs very slowly phases=[Phase.explicit, Phase.reuse, Phase.generate, Phase.target], ) @given( ElectionFactory.get_selection_description_well_formed(), elgamal_keypairs(), elements_mod_q_no_zero(), integers(), ) def test_decrypt_selection_valid_input_tampered_fails( self, selection_description: Tuple[str, SelectionDescription], keypair: ElGamalKeyPair, seed: ElementModQ, random_seed: int, ): # Arrange _, description = selection_description random = Random(random_seed) data = ballot_factory.get_random_selection_from(description, random) # Act subject = encrypt_selection( data, description, keypair.public_key, ONE_MOD_Q, seed ) # tamper with the encryption malformed_encryption = deepcopy(subject) malformed_encryption.ciphertext.pad = mult_p(subject.ciphertext.pad, TWO_MOD_P) # tamper with the proof malformed_proof = deepcopy(subject) altered_a0 = mult_p(subject.proof.proof_zero_pad, TWO_MOD_P) malformed_disjunctive = DisjunctiveChaumPedersenProof( altered_a0, malformed_proof.proof.proof_zero_data, malformed_proof.proof.proof_one_pad, malformed_proof.proof.proof_one_data, malformed_proof.proof.proof_zero_challenge, malformed_proof.proof.proof_one_challenge, malformed_proof.proof.challenge, malformed_proof.proof.proof_zero_response, malformed_proof.proof.proof_one_response, ) malformed_proof.proof = malformed_disjunctive result_from_key_malformed_encryption = decrypt_selection_with_secret( malformed_encryption, description, keypair.public_key, keypair.secret_key, ONE_MOD_Q, ) result_from_key_malformed_proof = decrypt_selection_with_secret( malformed_proof, description, keypair.public_key, keypair.secret_key, ONE_MOD_Q, ) result_from_nonce_malformed_encryption = decrypt_selection_with_nonce( malformed_encryption, description, keypair.public_key, ONE_MOD_Q ) result_from_nonce_malformed_proof = decrypt_selection_with_nonce( malformed_proof, description, keypair.public_key, ONE_MOD_Q ) # Assert self.assertIsNone(result_from_key_malformed_encryption) self.assertIsNone(result_from_key_malformed_proof) self.assertIsNone(result_from_nonce_malformed_encryption) self.assertIsNone(result_from_nonce_malformed_proof) @settings( deadline=timedelta(milliseconds=2000), suppress_health_check=[HealthCheck.too_slow], max_examples=10, # disabling the "shrink" phase, because it runs very slowly phases=[Phase.explicit, Phase.reuse, Phase.generate, Phase.target], ) @given( ElectionFactory.get_selection_description_well_formed(), elgamal_keypairs(), elements_mod_q_no_zero(), integers(), ) def test_decrypt_selection_tampered_nonce_fails( self, selection_description: Tuple[str, SelectionDescription], keypair: ElGamalKeyPair, nonce_seed: ElementModQ, random_seed: int, ): # Arrange random = Random(random_seed) _, description = selection_description data = ballot_factory.get_random_selection_from(description, random) # Act subject = encrypt_selection( data, description, keypair.public_key, ONE_MOD_Q, nonce_seed, should_verify_proofs=True, ) self.assertIsNotNone(subject) # Tamper with the nonce by setting it to an arbitrary value subject.nonce = nonce_seed result_from_nonce_seed = decrypt_selection_with_nonce( subject, description, keypair.public_key, ONE_MOD_Q, nonce_seed ) # Assert self.assertIsNone(result_from_nonce_seed) @settings( deadline=timedelta(milliseconds=5000), suppress_health_check=[HealthCheck.too_slow], max_examples=10, # disabling the "shrink" phase, because it runs very slowly phases=[Phase.explicit, Phase.reuse, Phase.generate, Phase.target], ) @given( ElectionFactory.get_contest_description_well_formed(), elgamal_keypairs(), elements_mod_q_no_zero(), integers(), ) def test_decrypt_contest_valid_input_succeeds( self, contest_description: Tuple[str, ContestDescription], keypair: ElGamalKeyPair, nonce_seed: ElementModQ, random_seed: int, ): # Arrange _, description = contest_description random = Random(random_seed) data = ballot_factory.get_random_contest_from(description, random) placeholders = generate_placeholder_selections_from( description, description.number_elected ) description_with_placeholders = contest_description_with_placeholders_from( description, placeholders ) self.assertTrue(description_with_placeholders.is_valid()) # Act subject = encrypt_contest( data, description_with_placeholders, keypair.public_key, ONE_MOD_Q, nonce_seed, should_verify_proofs=True, ) self.assertIsNotNone(subject) # Decrypt the contest, but keep the placeholders # so we can verify the selection count matches as expected in the test result_from_key = decrypt_contest_with_secret( subject, description_with_placeholders, keypair.public_key, keypair.secret_key, ONE_MOD_Q, remove_placeholders=False, ) result_from_nonce = decrypt_contest_with_nonce( subject, description_with_placeholders, keypair.public_key, ONE_MOD_Q, remove_placeholders=False, ) result_from_nonce_seed = decrypt_contest_with_nonce( subject, description_with_placeholders, keypair.public_key, ONE_MOD_Q, nonce_seed, remove_placeholders=False, ) # Assert self.assertIsNotNone(result_from_key) self.assertIsNotNone(result_from_nonce) self.assertIsNotNone(result_from_nonce_seed) # The decrypted contest should include an entry for each possible selection # and placeholders for each seat expected_entries = ( len(description.ballot_selections) + description.number_elected ) self.assertTrue( result_from_key.is_valid( description.object_id, expected_entries, description.number_elected, description.votes_allowed, ) ) self.assertTrue( result_from_nonce.is_valid( description.object_id, expected_entries, description.number_elected, description.votes_allowed, ) ) self.assertTrue( result_from_nonce_seed.is_valid( description.object_id, expected_entries, description.number_elected, description.votes_allowed, ) ) # Assert the ballot selections sum to the expected number of selections key_selected = sum( selection.vote for selection in result_from_key.ballot_selections ) nonce_selected = sum( selection.vote for selection in result_from_nonce.ballot_selections ) seed_selected = sum( selection.vote for selection in result_from_nonce_seed.ballot_selections ) self.assertEqual(key_selected, nonce_selected) self.assertEqual(seed_selected, nonce_selected) self.assertEqual(description.number_elected, key_selected) # Assert each selection is valid for selection_description in description.ballot_selections: key_selection = [ selection for selection in result_from_key.ballot_selections if selection.object_id == selection_description.object_id ][0] nonce_selection = [ selection for selection in result_from_nonce.ballot_selections if selection.object_id == selection_description.object_id ][0] seed_selection = [ selection for selection in result_from_nonce_seed.ballot_selections if selection.object_id == selection_description.object_id ][0] data_selections_exist = [ selection for selection in data.ballot_selections if selection.object_id == selection_description.object_id ] # It's possible there are no selections in the original data collection # since it is valid to pass in a ballot that is not complete if any(data_selections_exist): self.assertTrue(data_selections_exist[0].vote == key_selection.vote) self.assertTrue(data_selections_exist[0].vote == nonce_selection.vote) self.assertTrue(data_selections_exist[0].vote == seed_selection.vote) # TODO: also check edge cases such as: # - placeholder selections are true for under votes self.assertTrue(key_selection.is_valid(selection_description.object_id)) self.assertTrue(nonce_selection.is_valid(selection_description.object_id)) self.assertTrue(seed_selection.is_valid(selection_description.object_id)) @settings( deadline=timedelta(milliseconds=5000), suppress_health_check=[HealthCheck.too_slow], max_examples=10, # disabling the "shrink" phase, because it runs very slowly phases=[Phase.explicit, Phase.reuse, Phase.generate, Phase.target], ) @given( ElectionFactory.get_contest_description_well_formed(), elgamal_keypairs(), elements_mod_q_no_zero(), integers(), ) def test_decrypt_contest_invalid_input_fails( self, contest_description: Tuple[str, ContestDescription], keypair: ElGamalKeyPair, nonce_seed: ElementModQ, random_seed: int, ): # Arrange _, description = contest_description random = Random(random_seed) data = ballot_factory.get_random_contest_from(description, random) placeholders = generate_placeholder_selections_from( description, description.number_elected ) description_with_placeholders = contest_description_with_placeholders_from( description, placeholders ) self.assertTrue(description_with_placeholders.is_valid()) # Act subject = encrypt_contest( data, description_with_placeholders, keypair.public_key, ONE_MOD_Q, nonce_seed, ) self.assertIsNotNone(subject) # tamper with the nonce subject.nonce = ONE_MOD_Q result_from_nonce = decrypt_contest_with_nonce( subject, description_with_placeholders, keypair.public_key, ONE_MOD_Q, remove_placeholders=False, ) result_from_nonce_seed = decrypt_contest_with_nonce( subject, description_with_placeholders, keypair.public_key, ONE_MOD_Q, nonce_seed, remove_placeholders=False, ) # Assert self.assertIsNone(result_from_nonce) self.assertIsNone(result_from_nonce_seed) # Tamper with the encryption subject.ballot_selections[0].ciphertext = ElGamalCiphertext( TWO_MOD_P, TWO_MOD_P ) result_from_key_tampered = decrypt_contest_with_secret( subject, description_with_placeholders, keypair.public_key, keypair.secret_key, ONE_MOD_Q, remove_placeholders=False, ) result_from_nonce_tampered = decrypt_contest_with_nonce( subject, description_with_placeholders, keypair.public_key, ONE_MOD_Q, remove_placeholders=False, ) result_from_nonce_seed_tampered = decrypt_contest_with_nonce( subject, description_with_placeholders, keypair.public_key, ONE_MOD_Q, nonce_seed, remove_placeholders=False, ) # Assert self.assertIsNone(result_from_key_tampered) self.assertIsNone(result_from_nonce_tampered) self.assertIsNone(result_from_nonce_seed_tampered) @settings( deadline=timedelta(milliseconds=5000), suppress_health_check=[HealthCheck.too_slow], max_examples=1, # disabling the "shrink" phase, because it runs very slowly phases=[Phase.explicit, Phase.reuse, Phase.generate, Phase.target], ) @given(elgamal_keypairs()) def test_decrypt_ballot_valid_input_succeeds(self, keypair: ElGamalKeyPair): """ Check that decryption works as expected by encrypting a ballot using the stateful `EncryptionMediator` and then calling the various decrypt functions. """ # TODO: Hypothesis test instead # Arrange election = election_factory.get_simple_manifest_from_file() internal_manifest, context = election_factory.get_fake_ciphertext_election( election, keypair.public_key ) data = ballot_factory.get_simple_ballot_from_file() device = election_factory.get_encryption_device() operator = EncryptionMediator(internal_manifest, context, device) # Act subject = operator.encrypt(data) self.assertIsNotNone(subject) result_from_key = decrypt_ballot_with_secret( subject, internal_manifest, context.crypto_extended_base_hash, keypair.public_key, keypair.secret_key, remove_placeholders=False, ) result_from_nonce = decrypt_ballot_with_nonce( subject, internal_manifest, context.crypto_extended_base_hash, keypair.public_key, remove_placeholders=False, ) result_from_nonce_seed = decrypt_ballot_with_nonce( subject, internal_manifest, context.crypto_extended_base_hash, keypair.public_key, subject.nonce, remove_placeholders=False, ) # Assert self.assertIsNotNone(result_from_key) self.assertIsNotNone(result_from_nonce) self.assertIsNotNone(result_from_nonce_seed) self.assertEqual(data.object_id, subject.object_id) self.assertEqual(data.object_id, result_from_key.object_id) self.assertEqual(data.object_id, result_from_nonce.object_id) self.assertEqual(data.object_id, result_from_nonce_seed.object_id) for description in internal_manifest.get_contests_for(data.style_id): expected_entries = ( len(description.ballot_selections) + description.number_elected ) key_contest = [ contest for contest in result_from_key.contests if contest.object_id == description.object_id ][0] nonce_contest = [ contest for contest in result_from_nonce.contests if contest.object_id == description.object_id ][0] seed_contest = [ contest for contest in result_from_nonce_seed.contests if contest.object_id == description.object_id ][0] # Contests may not be voted on the ballot data_contest_exists = [ contest for contest in data.contests if contest.object_id == description.object_id ] if any(data_contest_exists): data_contest = data_contest_exists[0] else: data_contest = None self.assertTrue( key_contest.is_valid( description.object_id, expected_entries, description.number_elected, description.votes_allowed, ) ) self.assertTrue( nonce_contest.is_valid( description.object_id, expected_entries, description.number_elected, description.votes_allowed, ) ) self.assertTrue( seed_contest.is_valid( description.object_id, expected_entries, description.number_elected, description.votes_allowed, ) ) for selection_description in description.ballot_selections: key_selection = [ selection for selection in key_contest.ballot_selections if selection.object_id == selection_description.object_id ][0] nonce_selection = [ selection for selection in nonce_contest.ballot_selections if selection.object_id == selection_description.object_id ][0] seed_selection = [ selection for selection in seed_contest.ballot_selections if selection.object_id == selection_description.object_id ][0] # Selections may be undervoted for a specific contest if any(data_contest_exists): data_selection_exists = [ selection for selection in data_contest.ballot_selections if selection.object_id == selection_description.object_id ] else: data_selection_exists = [] if any(data_selection_exists): data_selection = data_selection_exists[0] self.assertTrue(data_selection.vote == key_selection.vote) self.assertTrue(data_selection.vote == nonce_selection.vote) self.assertTrue(data_selection.vote == seed_selection.vote) else: data_selection = None # TODO: also check edge cases such as: # - placeholder selections are true for under votes self.assertTrue(key_selection.is_valid(selection_description.object_id)) self.assertTrue( nonce_selection.is_valid(selection_description.object_id) ) self.assertTrue( seed_selection.is_valid(selection_description.object_id) ) @settings( deadline=timedelta(milliseconds=2000), suppress_health_check=[HealthCheck.too_slow], max_examples=1, # disabling the "shrink" phase, because it runs very slowly phases=[Phase.explicit, Phase.reuse, Phase.generate, Phase.target], ) @given(elgamal_keypairs()) def test_decrypt_ballot_valid_input_missing_nonce_fails( self, keypair: ElGamalKeyPair ): # Arrange election = election_factory.get_simple_manifest_from_file() internal_manifest, context = election_factory.get_fake_ciphertext_election( election, keypair.public_key ) data = ballot_factory.get_simple_ballot_from_file() device = election_factory.get_encryption_device() operator = EncryptionMediator(internal_manifest, context, device) # Act subject = operator.encrypt(data) self.assertIsNotNone(subject) subject.nonce = None missing_nonce_value = None result_from_nonce = decrypt_ballot_with_nonce( subject, internal_manifest, context.crypto_extended_base_hash, keypair.public_key, ) # SUGGEST this test is the same as the one above result_from_nonce_seed = decrypt_ballot_with_nonce( subject, internal_manifest, context.crypto_extended_base_hash, keypair.public_key, missing_nonce_value, ) # Assert self.assertIsNone(result_from_nonce) self.assertIsNone(result_from_nonce_seed) ================================================ FILE: tests/property/test_decryption_mediator.py ================================================ # pylint: disable=too-many-instance-attributes from datetime import timedelta from typing import Dict, List from random import randrange from hypothesis import given, HealthCheck, settings, Phase from hypothesis.strategies import integers, data from tests.base_test_case import BaseTestCase from electionguard.ballot import PlaintextBallot from electionguard.ballot_box import BallotBox, BallotBoxState, cast_ballot, get_ballots from electionguard.data_store import DataStore from electionguard.decryption_mediator import DecryptionMediator from electionguard.election import CiphertextElectionContext from electionguard.encrypt import ( EncryptionMediator, encrypt_ballot, ) from electionguard.group import ONE_MOD_Q from electionguard.guardian import Guardian from electionguard.key_ceremony import CeremonyDetails from electionguard.key_ceremony_mediator import KeyCeremonyMediator from electionguard.manifest import InternalManifest from electionguard.tally import ( CiphertextTally, PlaintextTally, tally_ballots, ) from electionguard.utils import get_optional import electionguard_tools.factories.ballot_factory as BallotFactory import electionguard_tools.factories.election_factory as ElectionFactory from electionguard_tools.strategies.election import ( election_descriptions, plaintext_voted_ballots, ) from electionguard_tools.helpers.tally_ceremony_orchestrator import ( TallyCeremonyOrchestrator, ) from electionguard_tools.helpers.key_ceremony_orchestrator import ( KeyCeremonyOrchestrator, ) from electionguard_tools.helpers.tally_accumulate import accumulate_plaintext_ballots from electionguard_tools.helpers.election_builder import ElectionBuilder election_factory = ElectionFactory.ElectionFactory() ballot_factory = BallotFactory.BallotFactory() class TestDecryptionMediator(BaseTestCase): """Test suite for DecryptionMediator""" NUMBER_OF_GUARDIANS = 3 QUORUM = 2 CEREMONY_DETAILS = CeremonyDetails(NUMBER_OF_GUARDIANS, QUORUM) internal_manifest: InternalManifest decryption_mediator_id = "mediator-id" def setUp(self): # Key Ceremony key_ceremony_mediator = KeyCeremonyMediator( "key_ceremony_mediator_mediator", self.CEREMONY_DETAILS ) self.guardians: List[Guardian] = KeyCeremonyOrchestrator.create_guardians( self.CEREMONY_DETAILS ) KeyCeremonyOrchestrator.perform_full_ceremony( self.guardians, key_ceremony_mediator ) self.joint_public_key = key_ceremony_mediator.publish_joint_key() self.assertIsNotNone(self.joint_public_key) # Setup the election self.manifest = election_factory.get_fake_manifest() builder = ElectionBuilder(self.NUMBER_OF_GUARDIANS, self.QUORUM, self.manifest) self.assertIsNone(builder.build()) # Can't build without the public key builder.set_public_key(self.joint_public_key.joint_public_key) builder.set_commitment_hash(self.joint_public_key.commitment_hash) self.internal_manifest, self.context = get_optional(builder.build()) self.encryption_device = election_factory.get_encryption_device() self.ballot_marking_device = EncryptionMediator( self.internal_manifest, self.context, self.encryption_device ) # get some fake ballots self.fake_cast_ballot = ballot_factory.get_fake_ballot( self.internal_manifest, "some-unique-ballot-id-cast" ) more_fake_ballots = [] for i in range(10): more_fake_ballots.append( ballot_factory.get_fake_ballot( self.internal_manifest, f"some-unique-ballot-id-cast{i}" ) ) self.fake_spoiled_ballot = ballot_factory.get_fake_ballot( self.internal_manifest, "some-unique-ballot-id-spoiled" ) more_fake_spoiled_ballots = [] for i in range(2): more_fake_spoiled_ballots.append( ballot_factory.get_fake_ballot( self.internal_manifest, f"some-unique-ballot-id-spoiled{i}" ) ) self.assertTrue( self.fake_cast_ballot.is_valid( self.internal_manifest.ballot_styles[0].object_id ) ) self.assertTrue( self.fake_spoiled_ballot.is_valid( self.internal_manifest.ballot_styles[0].object_id ) ) self.expected_plaintext_tally = accumulate_plaintext_ballots( [self.fake_cast_ballot] + more_fake_ballots ) # Fill in the expected values with any missing selections # that were not made on any ballots selection_ids = { selection.object_id for contest in self.internal_manifest.contests for selection in contest.ballot_selections } missing_selection_ids = selection_ids.difference( set(self.expected_plaintext_tally) ) for id in missing_selection_ids: self.expected_plaintext_tally[id] = 0 # Encrypt self.encrypted_fake_cast_ballot = self.ballot_marking_device.encrypt( self.fake_cast_ballot ) self.encrypted_fake_spoiled_ballot = self.ballot_marking_device.encrypt( self.fake_spoiled_ballot ) self.assertIsNotNone(self.encrypted_fake_cast_ballot) self.assertIsNotNone(self.encrypted_fake_spoiled_ballot) self.assertTrue( self.encrypted_fake_cast_ballot.is_valid_encryption( self.internal_manifest.manifest_hash, self.joint_public_key.joint_public_key, self.context.crypto_extended_base_hash, ) ) # encrypt some more fake ballots more_fake_encrypted_ballots = [] for fake_ballot in more_fake_ballots: more_fake_encrypted_ballots.append( self.ballot_marking_device.encrypt(fake_ballot) ) # encrypt some more fake ballots self.more_fake_encrypted_spoiled_ballots = [] for fake_ballot in more_fake_spoiled_ballots: self.more_fake_encrypted_spoiled_ballots.append( self.ballot_marking_device.encrypt(fake_ballot) ) # configure the ballot box ballot_store = DataStore() ballot_box = BallotBox(self.internal_manifest, self.context, ballot_store) ballot_box.cast(self.encrypted_fake_cast_ballot) ballot_box.spoil(self.encrypted_fake_spoiled_ballot) # Cast some more fake ballots for fake_ballot in more_fake_encrypted_ballots: ballot_box.cast(fake_ballot) # Spoil some more fake ballots for fake_ballot in self.more_fake_encrypted_spoiled_ballots: ballot_box.spoil(fake_ballot) # generate encrypted tally self.ciphertext_tally = tally_ballots( ballot_store, self.internal_manifest, self.context ) self.ciphertext_ballots = list( get_ballots(ballot_store, BallotBoxState.SPOILED).values() ) def test_announce(self): # Arrange mediator = DecryptionMediator( self.decryption_mediator_id, self.context, ) guardian = self.guardians[0] guardian_key = self.guardians[0].share_key() tally_share = guardian.compute_tally_share(self.ciphertext_tally, self.context) ballot_shares = {} # Act mediator.announce(guardian_key, tally_share, ballot_shares) # Assert self.assertEqual(len(mediator.get_available_guardians()), 1) # Act # Announce again mediator.announce(guardian_key, tally_share, ballot_shares) # Assert # Can only announce once self.assertEqual(len(mediator.get_available_guardians()), 1) # Cannot get plaintext tally or spoiled ballots without a quorum self.assertIsNone( mediator.get_plaintext_tally(self.ciphertext_tally, self.manifest) ) self.assertIsNone( mediator.get_plaintext_ballots(self.ciphertext_ballots, self.manifest) ) def test_get_plaintext_with_all_guardians_present(self): # Arrange mediator = DecryptionMediator( self.decryption_mediator_id, self.context, ) available_guardians = self.guardians TallyCeremonyOrchestrator.perform_decryption_setup( available_guardians, mediator, self.context, self.ciphertext_tally, self.ciphertext_ballots, ) # Act plaintext_tally = mediator.get_plaintext_tally( self.ciphertext_tally, self.manifest ) plaintext_ballots = mediator.get_plaintext_ballots( self.ciphertext_ballots, self.manifest ) # Convert to selections to check for the same tally selections = _convert_to_selections(plaintext_tally) # Verify we get the same tally back if we call again another_plaintext_tally = mediator.get_plaintext_tally( self.ciphertext_tally, self.manifest ) # Assert self.assertIsNotNone(plaintext_tally) self.assertIsNotNone(plaintext_ballots) self.assertEqual(len(self.ciphertext_ballots), len(plaintext_ballots)) self.assertIsNotNone(selections) self.assertEqual(self.expected_plaintext_tally, selections) self.assertEqual(plaintext_tally, another_plaintext_tally) def test_get_plaintext_with_a_missing_guardian(self): # Arrange mediator = DecryptionMediator( self.decryption_mediator_id, self.context, ) available_guardians = self.guardians[0:2] all_guardian_keys = [guardian.share_key() for guardian in self.guardians] TallyCeremonyOrchestrator.perform_compensated_decryption_setup( available_guardians, all_guardian_keys, mediator, self.context, self.ciphertext_tally, self.ciphertext_ballots, ) # Act plaintext_tally = mediator.get_plaintext_tally( self.ciphertext_tally, self.manifest ) plaintext_ballots = mediator.get_plaintext_ballots( self.ciphertext_ballots, self.manifest ) # Convert to selections to check for the same tally selections = _convert_to_selections(plaintext_tally) # Verify we get the same tally back if we call again another_plaintext_tally = mediator.get_plaintext_tally( self.ciphertext_tally, self.manifest ) # Assert self.assertIsNotNone(plaintext_tally) self.assertIsNotNone(plaintext_ballots) self.assertEqual(len(self.ciphertext_ballots), len(plaintext_ballots)) self.assertIsNotNone(selections) self.assertEqual(self.expected_plaintext_tally, selections) self.assertEqual(plaintext_tally, another_plaintext_tally) @settings( deadline=timedelta(milliseconds=15000), suppress_health_check=[HealthCheck.too_slow], max_examples=8, # disabling the "shrink" phase, because it runs very slowly phases=[Phase.explicit, Phase.reuse, Phase.generate, Phase.target], ) @given(data(), integers(1, 3), integers(2, 5)) def test_get_plaintext_tally_with_all_guardians_present( self, values, parties: int, contests: int ): # Arrange manifest = values.draw(election_descriptions(parties, contests)) builder = ElectionBuilder(self.NUMBER_OF_GUARDIANS, self.QUORUM, manifest) internal_manifest, context = ( builder.set_public_key(self.joint_public_key.joint_public_key) .set_commitment_hash(self.joint_public_key.commitment_hash) .build() ) plaintext_ballots: List[PlaintextBallot] = values.draw( plaintext_voted_ballots(internal_manifest, randrange(3, 6)) ) expected_plaintext_tally = accumulate_plaintext_ballots(plaintext_ballots) encrypted_tally = self._generate_encrypted_tally( internal_manifest, context, plaintext_ballots ) mediator = DecryptionMediator(self.decryption_mediator_id, context) available_guardians = self.guardians TallyCeremonyOrchestrator.perform_decryption_setup( available_guardians, mediator, context, encrypted_tally, [] ) # Act plaintext_tally = mediator.get_plaintext_tally(encrypted_tally, manifest) selections = _convert_to_selections(plaintext_tally) # Assert self.assertIsNotNone(plaintext_tally) self.assertIsNotNone(selections) self.assertEqual(expected_plaintext_tally, selections) def _generate_encrypted_tally( self, internal_manifest: InternalManifest, context: CiphertextElectionContext, ballots: List[PlaintextBallot], ) -> CiphertextTally: # encrypt each ballot store = DataStore() for ballot in ballots: encrypted_ballot = encrypt_ballot( ballot, internal_manifest, context, ONE_MOD_Q, should_verify_proofs=True ) self.assertIsNotNone(encrypted_ballot) # add to the ballot store store.set( encrypted_ballot.object_id, cast_ballot(encrypted_ballot), ) tally = tally_ballots(store, internal_manifest, context) self.assertIsNotNone(tally) return get_optional(tally) def _convert_to_selections(tally: PlaintextTally) -> Dict[str, int]: plaintext_selections: Dict[str, int] = {} for _, contest in tally.contests.items(): for selection_id, selection in contest.selections.items(): plaintext_selections[selection_id] = selection.tally return plaintext_selections ================================================ FILE: tests/property/test_discrete_log.py ================================================ import asyncio from hypothesis import given from hypothesis.strategies import integers from tests.base_test_case import BaseTestCase from electionguard.constants import get_generator, get_large_prime from electionguard.discrete_log import ( compute_discrete_log, compute_discrete_log_async, DiscreteLog, precompute_discrete_log_cache, ) from electionguard.group import ( ElementModP, ElementModQ, ONE_MOD_P, ONE_MOD_Q, mult_p, g_pow_p, ) def _discrete_log_uncached(e: ElementModP) -> int: """ A simpler implementation of discrete_log, only meant for comparison testing of the caching version. """ count = 0 g_inv = ElementModP(pow(get_generator(), -1, get_large_prime()), False) while e != ONE_MOD_P: e = mult_p(e, g_inv) count = count + 1 return count class TestDiscreteLogFunctions(BaseTestCase): """Discrete log tests""" @given(integers(0, 100)) def test_uncached(self, exp: int) -> None: # Arrange plaintext = ElementModQ(exp) exp_plaintext = g_pow_p(plaintext) # Act plaintext_again = _discrete_log_uncached(exp_plaintext) # Assert self.assertEqual(plaintext, plaintext_again) @given(integers(0, 1000)) def test_cached(self, exp: int) -> None: # Arrange cache = {ONE_MOD_P: 0} plaintext = ElementModQ(exp) exp_plaintext = g_pow_p(plaintext) # Act (plaintext_again, returned_cache) = compute_discrete_log(exp_plaintext, cache) # Assert self.assertEqual(plaintext, plaintext_again) self.assertEqual(len(cache), len(returned_cache)) def test_cached_one(self) -> None: cache = {ONE_MOD_P: 0} plaintext = ONE_MOD_Q ciphertext = g_pow_p(plaintext) (plaintext_again, returned_cache) = compute_discrete_log(ciphertext, cache) self.assertEqual(plaintext, plaintext_again) self.assertEqual(len(cache), len(returned_cache)) def test_cached_one_async(self) -> None: # Arrange cache = {ONE_MOD_P: 0} plaintext = ONE_MOD_Q ciphertext = g_pow_p(plaintext) # Act loop = asyncio.new_event_loop() (plaintext_again, returned_cache) = loop.run_until_complete( compute_discrete_log_async(ciphertext, cache) ) loop.close() # Assert self.assertEqual(plaintext, plaintext_again) self.assertEqual(len(cache), len(returned_cache)) @given(integers(0, 1000)) def test_precompute_discrete_log(self, exponent: int) -> None: # Arrange minimum_cache_size = exponent + 1 element = g_pow_p(exponent) # Act cache = precompute_discrete_log_cache(exponent) (calculated_exponent, _returned_cache) = compute_discrete_log(element, cache) # Assert self.assertGreaterEqual(len(cache), minimum_cache_size) self.assertEqual(exponent, calculated_exponent) class TestDiscreteLogClass(BaseTestCase): """Discrete log tests""" @given(integers(0, 1000)) def test_precompute(self, exponent: int) -> None: # Arrange # Due to Singleton it could be bigger on previous run. minimum_cache_size = exponent + 1 element = g_pow_p(exponent) # Act DiscreteLog().set_lazy_evaluation(False) DiscreteLog().precompute_cache(exponent) calculated_exponent = DiscreteLog().discrete_log(element) # Assert self.assertGreaterEqual(len(DiscreteLog().get_cache()), minimum_cache_size) self.assertEqual(exponent, calculated_exponent) @given(integers(0, 1000)) def test_cached(self, exp: int) -> None: # Arrange plaintext = ElementModQ(exp) exp_plaintext = g_pow_p(plaintext) # Act plaintext_again = DiscreteLog().discrete_log(exp_plaintext) # Assert self.assertEqual(plaintext, plaintext_again) def test_cached_one(self) -> None: # Arrange plaintext = ONE_MOD_Q ciphertext = g_pow_p(plaintext) # Act plaintext_again = DiscreteLog().discrete_log(ciphertext) # Assert self.assertEqual(plaintext, plaintext_again) def test_cached_one_async(self) -> None: # Arrange plaintext = ONE_MOD_Q ciphertext = g_pow_p(plaintext) # Act loop = asyncio.new_event_loop() plaintext_again = loop.run_until_complete( DiscreteLog().discrete_log_async(ciphertext) ) loop.close() # Assert self.assertEqual(plaintext, plaintext_again) ================================================ FILE: tests/property/test_elgamal.py ================================================ from timeit import default_timer as timer from hypothesis import given from hypothesis.strategies import integers from tests.base_test_case import BaseTestCase from electionguard.constants import ( get_generator, get_small_prime, get_large_prime, ) from electionguard.elgamal import ( ElGamalKeyPair, elgamal_encrypt, elgamal_add, elgamal_keypair_from_secret, elgamal_keypair_random, elgamal_combine_public_keys, hashed_elgamal_encrypt, ) from electionguard.encrypt import ContestData from electionguard.group import ( ElementModQ, g_pow_p, ZERO_MOD_Q, TWO_MOD_Q, ONE_MOD_Q, ONE_MOD_P, ) from electionguard.logs import log_info from electionguard.nonces import Nonces from electionguard.serialize import padded_decode from electionguard.scheduler import Scheduler from electionguard.utils import ContestErrorType, get_optional from electionguard_tools.strategies.elgamal import elgamal_keypairs from electionguard_tools.strategies.group import elements_mod_q_no_zero class TestElGamal(BaseTestCase): """ElGamal tests""" def test_simple_elgamal_encryption_decryption(self) -> None: nonce = ONE_MOD_Q secret_key = TWO_MOD_Q keypair = get_optional(elgamal_keypair_from_secret(secret_key)) public_key = keypair.public_key self.assertLess(public_key, get_large_prime()) elem = g_pow_p(ZERO_MOD_Q) self.assertEqual(elem, ONE_MOD_P) # g^0 == 1 ciphertext = get_optional(elgamal_encrypt(0, nonce, keypair.public_key)) self.assertEqual(get_generator(), ciphertext.pad) self.assertEqual( pow(ciphertext.pad.value, secret_key.value, get_large_prime()), pow(public_key.value, nonce.value, get_large_prime()), ) self.assertEqual( ciphertext.data.value, pow(public_key.value, nonce.value, get_large_prime()), ) plaintext = ciphertext.decrypt(keypair.secret_key) self.assertEqual(0, plaintext) @given(integers(0, 100), elgamal_keypairs()) def test_elgamal_encrypt_requires_nonzero_nonce( self, message: int, keypair: ElGamalKeyPair ) -> None: self.assertEqual(None, elgamal_encrypt(message, ZERO_MOD_Q, keypair.public_key)) def test_elgamal_keypair_from_secret_requires_key_greater_than_one(self) -> None: self.assertEqual(None, elgamal_keypair_from_secret(ZERO_MOD_Q)) self.assertEqual(None, elgamal_keypair_from_secret(ONE_MOD_Q)) @given(integers(0, 100), elements_mod_q_no_zero(), elgamal_keypairs()) def test_elgamal_encryption_decryption_inverses( self, message: int, nonce: ElementModQ, keypair: ElGamalKeyPair ) -> None: ciphertext = get_optional(elgamal_encrypt(message, nonce, keypair.public_key)) plaintext = ciphertext.decrypt(keypair.secret_key) self.assertEqual(message, plaintext) @given(integers(0, 100), elements_mod_q_no_zero(), elgamal_keypairs()) def test_elgamal_encryption_decryption_with_known_nonce_inverses( self, message: int, nonce: ElementModQ, keypair: ElGamalKeyPair ) -> None: ciphertext = get_optional(elgamal_encrypt(message, nonce, keypair.public_key)) plaintext = ciphertext.decrypt_known_nonce(keypair.public_key, nonce) self.assertEqual(message, plaintext) @given(elgamal_keypairs()) def test_elgamal_generated_keypairs_are_within_range( self, keypair: ElGamalKeyPair ) -> None: self.assertLess(keypair.public_key, get_large_prime()) self.assertLess(keypair.secret_key, get_small_prime()) self.assertEqual(g_pow_p(keypair.secret_key), keypair.public_key) @given( elgamal_keypairs(), integers(0, 100), elements_mod_q_no_zero(), integers(0, 100), elements_mod_q_no_zero(), ) def test_elgamal_add_homomorphic_accumulation_decrypts_successfully( self, keypair: ElGamalKeyPair, m1: int, r1: ElementModQ, m2: int, r2: ElementModQ, ) -> None: c1 = get_optional(elgamal_encrypt(m1, r1, keypair.public_key)) c2 = get_optional(elgamal_encrypt(m2, r2, keypair.public_key)) c_sum = elgamal_add(c1, c2) total = c_sum.decrypt(keypair.secret_key) self.assertEqual(total, m1 + m2) def test_elgamal_add_requires_args(self) -> None: self.assertRaises(Exception, elgamal_add) @given(elgamal_keypairs()) def test_elgamal_keypair_produces_valid_residue(self, keypair) -> None: self.assertTrue(keypair.public_key.is_valid_residue()) def test_elgamal_keypair_random(self) -> None: # Act random_keypair = elgamal_keypair_random() random_keypair_two = elgamal_keypair_random() # Assert self.assertIsNotNone(random_keypair) self.assertIsNotNone(random_keypair.public_key) self.assertIsNotNone(random_keypair.secret_key) self.assertNotEqual(random_keypair, random_keypair_two) def test_elgamal_combine_public_keys(self) -> None: # Arrange random_keypair = elgamal_keypair_random() random_keypair_two = elgamal_keypair_random() public_keys = [random_keypair.public_key, random_keypair_two.public_key] # Act joint_key = elgamal_combine_public_keys(public_keys) # Assert self.assertIsNotNone(joint_key) self.assertNotEqual(joint_key, random_keypair.public_key) self.assertNotEqual(joint_key, random_keypair_two.public_key) def test_gmpy2_parallelism_is_safe(self) -> None: """ Ensures running lots of parallel exponentiations still yields the correct answer. This verifies that nothing incorrect is happening in the GMPY2 library """ # Arrange scheduler = Scheduler() problem_size = 1000 random_secret_keys = Nonces(ElementModQ(3))[0:problem_size] log_info( f"testing GMPY2 powmod parallelism safety (cpus = {scheduler.cpu_count}, problem_size = {problem_size})" ) # Act start = timer() keypairs = scheduler.schedule( elgamal_keypair_from_secret, [list([secret_key]) for secret_key in random_secret_keys], ) end1 = timer() # Assert for keypair in keypairs: self.assertEqual( keypair.public_key, elgamal_keypair_from_secret(keypair.secret_key).public_key, ) end2 = timer() scheduler.close() log_info(f"Parallelism speedup: {(end2 - end1) / (end1 - start):.3f}") def test_hashed_elgamal_encryption(self) -> None: """ Ensure Hashed ElGamal encrypts and decrypts as expected. """ # Arrange message = ContestData(ContestErrorType.Default) keypair = elgamal_keypair_random() nonce = ONE_MOD_Q seed = ONE_MOD_Q # Act padded_message = message.to_bytes() encrypted_message = hashed_elgamal_encrypt( padded_message, nonce, keypair.public_key, seed ) decrypted_message = encrypted_message.decrypt(keypair.secret_key, seed) if decrypted_message is not None: unpadded_message = padded_decode(ContestData, decrypted_message) # Assert self.assertIsNotNone(encrypted_message) self.assertIsNotNone(decrypted_message) if decrypted_message is not None: self.assertEqual(padded_message, decrypted_message) self.assertEqual(message, unpadded_message) ================================================ FILE: tests/property/test_encrypt.py ================================================ from unittest import skip from unittest.mock import patch from copy import deepcopy from datetime import timedelta from random import Random from secrets import randbelow from typing import Tuple from hypothesis import HealthCheck from hypothesis import given, settings from hypothesis.strategies import integers from tests.base_test_case import BaseTestCase import electionguard_tools.factories.ballot_factory as BallotFactory import electionguard_tools.factories.election_factory as ElectionFactory from electionguard_tools.strategies.elgamal import elgamal_keypairs from electionguard_tools.strategies.group import elements_mod_q_no_zero from electionguard.constants import get_small_prime from electionguard.chaum_pedersen import ( ConstantChaumPedersenProof, DisjunctiveChaumPedersenProof, make_constant_chaum_pedersen, make_disjunctive_chaum_pedersen, ) from electionguard.elgamal import ( ElGamalKeyPair, elgamal_keypair_from_secret, elgamal_add, ) from electionguard.encrypt import ( EncryptionDevice, encrypt_ballot, encrypt_contest, encrypt_selection, selection_from, EncryptionMediator, ) from electionguard.group import ( ElementModQ, ONE_MOD_Q, TWO_MOD_Q, int_to_q, add_q, TWO_MOD_P, mult_p, ) from electionguard.manifest import ( ContestDescription, contest_description_with_placeholders_from, generate_placeholder_selections_from, SelectionDescription, VoteVariationType, ) election_factory = ElectionFactory.ElectionFactory() ballot_factory = BallotFactory.BallotFactory() SEED = election_factory.get_encryption_device().get_hash() class TestEncrypt(BaseTestCase): """Encryption tests""" def test_encrypt_simple_selection_succeeds(self): # Arrange keypair = elgamal_keypair_from_secret(int_to_q(2)) nonce = randbelow(get_small_prime()) metadata = SelectionDescription( "some-selection-object-id", 1, "some-candidate-id" ) hash_context = metadata.crypto_hash() subject = selection_from(metadata) self.assertTrue(subject.is_valid(metadata.object_id)) # Act result = encrypt_selection( subject, metadata, keypair.public_key, ONE_MOD_Q, nonce, should_verify_proofs=True, ) # Assert self.assertIsNotNone(result) self.assertIsNotNone(result.ciphertext) self.assertTrue( result.is_valid_encryption(hash_context, keypair.public_key, ONE_MOD_Q) ) def test_encrypt_simple_selection_malformed_data_fails(self): # Arrange keypair = elgamal_keypair_from_secret(int_to_q(2)) nonce = randbelow(get_small_prime()) metadata = SelectionDescription( "some-selection-object-id", 1, "some-candidate-id" ) hash_context = metadata.crypto_hash() subject = selection_from(metadata) self.assertTrue(subject.is_valid(metadata.object_id)) # Act result = encrypt_selection( subject, metadata, keypair.public_key, ONE_MOD_Q, nonce ) # tamper with the description_hash malformed_description_hash = deepcopy(result) malformed_description_hash.description_hash = TWO_MOD_Q # remove the proof missing_proof = deepcopy(result) missing_proof.proof = None # Assert self.assertFalse( malformed_description_hash.is_valid_encryption( hash_context, keypair.public_key, ONE_MOD_Q ) ) self.assertFalse( missing_proof.is_valid_encryption( hash_context, keypair.public_key, ONE_MOD_Q ) ) @settings( deadline=timedelta(milliseconds=2000), suppress_health_check=[HealthCheck.too_slow], max_examples=10, ) @given( ElectionFactory.get_selection_description_well_formed(), elgamal_keypairs(), elements_mod_q_no_zero(), integers(), ) def test_encrypt_selection_valid_input_succeeds( self, selection_description: Tuple[str, SelectionDescription], keypair: ElGamalKeyPair, seed: ElementModQ, random_seed: int, ): # Arrange _, description = selection_description random = Random(random_seed) subject = ballot_factory.get_random_selection_from(description, random) # Act result = encrypt_selection( subject, description, keypair.public_key, ONE_MOD_Q, seed, should_verify_proofs=True, ) # Assert self.assertIsNotNone(result) self.assertIsNotNone(result.ciphertext) self.assertTrue( result.is_valid_encryption( description.crypto_hash(), keypair.public_key, ONE_MOD_Q ) ) @settings( deadline=timedelta(milliseconds=2000), suppress_health_check=[HealthCheck.too_slow], max_examples=10, ) @given( ElectionFactory.get_selection_description_well_formed(), elgamal_keypairs(), elements_mod_q_no_zero(), integers(), ) def test_encrypt_selection_valid_input_tampered_encryption_fails( self, selection_description: Tuple[str, SelectionDescription], keypair: ElGamalKeyPair, seed: ElementModQ, random_seed: int, ): # Arrange _, description = selection_description random = Random(random_seed) subject = ballot_factory.get_random_selection_from(description, random) # Act result = encrypt_selection( subject, description, keypair.public_key, ONE_MOD_Q, seed, should_verify_proofs=False, ) self.assertTrue( result.is_valid_encryption( description.crypto_hash(), keypair.public_key, ONE_MOD_Q ) ) # tamper with the encryption malformed_encryption = deepcopy(result) malformed_encryption.ciphertext.pad = mult_p(result.ciphertext.pad, TWO_MOD_P) # tamper with the proof malformed_proof = deepcopy(result) altered_a0 = mult_p(result.proof.proof_zero_pad, TWO_MOD_P) malformed_disjunctive = DisjunctiveChaumPedersenProof( altered_a0, malformed_proof.proof.proof_zero_data, malformed_proof.proof.proof_one_pad, malformed_proof.proof.proof_one_data, malformed_proof.proof.proof_zero_challenge, malformed_proof.proof.proof_one_challenge, malformed_proof.proof.challenge, malformed_proof.proof.proof_zero_response, malformed_proof.proof.proof_one_response, ) malformed_proof.proof = malformed_disjunctive # Assert self.assertFalse( malformed_encryption.is_valid_encryption( description.crypto_hash(), keypair.public_key, ONE_MOD_Q ) ) self.assertFalse( malformed_proof.is_valid_encryption( description.crypto_hash(), keypair.public_key, ONE_MOD_Q ) ) @settings( deadline=timedelta(milliseconds=4000), suppress_health_check=[HealthCheck.too_slow], max_examples=10, ) @given( ElectionFactory.get_contest_description_well_formed(), elgamal_keypairs(), elements_mod_q_no_zero(), integers(), ) def test_encrypt_contest_valid_input_succeeds( self, contest_description: Tuple[str, ContestDescription], keypair: ElGamalKeyPair, nonce_seed: ElementModQ, random_seed: int, ): # Arrange _id, description = contest_description random = Random(random_seed) subject = ballot_factory.get_random_contest_from(description, random) # Act result = encrypt_contest( subject, description, keypair.public_key, ONE_MOD_Q, nonce_seed, should_verify_proofs=True, ) # Assert self.assertIsNotNone(result) self.assertTrue( result.is_valid_encryption( description.crypto_hash(), keypair.public_key, ONE_MOD_Q ) ) # The encrypted contest should include an entry for each possible selection # and placeholders for each seat expected_entries = ( len(description.ballot_selections) + description.number_elected ) self.assertEqual(len(result.ballot_selections), expected_entries) @settings( deadline=timedelta(milliseconds=2000), suppress_health_check=[HealthCheck.too_slow], max_examples=10, ) @given( ElectionFactory.get_contest_description_well_formed(), elgamal_keypairs(), elements_mod_q_no_zero(), integers(), ) def test_encrypt_contest_valid_input_tampered_proof_fails( self, contest_description: Tuple[str, ContestDescription], keypair: ElGamalKeyPair, nonce_seed: ElementModQ, random_seed: int, ): # Arrange _id, description = contest_description random = Random(random_seed) subject = ballot_factory.get_random_contest_from(description, random) # Act result = encrypt_contest( subject, description, keypair.public_key, ONE_MOD_Q, nonce_seed ) self.assertTrue( result.is_valid_encryption( description.crypto_hash(), keypair.public_key, ONE_MOD_Q ) ) # tamper with the proof malformed_proof = deepcopy(result) altered_a = mult_p(result.proof.pad, TWO_MOD_P) malformed_disjunctive = ConstantChaumPedersenProof( altered_a, malformed_proof.proof.data, malformed_proof.proof.challenge, malformed_proof.proof.response, malformed_proof.proof.constant, ) malformed_proof.proof = malformed_disjunctive # remove the proof missing_proof = deepcopy(result) missing_proof.proof = None # Assert self.assertFalse( malformed_proof.is_valid_encryption( description.crypto_hash(), keypair.public_key, ONE_MOD_Q ) ) self.assertFalse( missing_proof.is_valid_encryption( description.crypto_hash(), keypair.public_key, ONE_MOD_Q ) ) @skip("runs forever") @settings( deadline=timedelta(milliseconds=2000), suppress_health_check=[HealthCheck.too_slow], max_examples=10, ) @given( ElectionFactory.get_contest_description_well_formed(), elgamal_keypairs(), elements_mod_q_no_zero(), integers(1, 6), integers(), ) def test_encrypt_contest_overvote_fails( self, contest_description: Tuple[str, ContestDescription], keypair: ElGamalKeyPair, seed: ElementModQ, overvotes: int, random_seed: int, ): # Arrange _id, description = contest_description random = Random(random_seed) subject = ballot_factory.get_random_contest_from(description, random) for _i in range(overvotes): overvote = ballot_factory.get_random_selection_from( description.ballot_selections[0], random ) subject.ballot_selections.append(overvote) # Act result = encrypt_contest( subject, description, keypair.public_key, ONE_MOD_Q, seed ) # Assert self.assertIsNone(result) def test_encrypt_contest_manually_formed_contest_description_valid_succeeds(self): description = ContestDescription( object_id="0@A.com-contest", electoral_district_id="0@A.com-gp-unit", sequence_order=1, vote_variation=VoteVariationType.n_of_m, number_elected=1, votes_allowed=1, name="", ballot_selections=[ SelectionDescription( "0@A.com-selection", 0, "0@A.com", ), SelectionDescription("0@B.com-selection", 1, "0@B.com"), ], ballot_title=None, ballot_subtitle=None, ) keypair = elgamal_keypair_from_secret(TWO_MOD_Q) seed = ONE_MOD_Q #################### data = ballot_factory.get_random_contest_from(description, Random(0)) placeholders = generate_placeholder_selections_from( description, description.number_elected ) description_with_placeholders = contest_description_with_placeholders_from( description, placeholders ) # Act subject = encrypt_contest( data, description_with_placeholders, keypair.public_key, ONE_MOD_Q, seed, should_verify_proofs=True, ) self.assertIsNotNone(subject) def test_encrypt_contest_duplicate_selection_object_ids_fails(self): """ This is an example test of a failing test where the contest description is malformed """ description = ContestDescription( object_id="0@A.com-contest", electoral_district_id="0@A.com-gp-unit", sequence_order=1, vote_variation=VoteVariationType.n_of_m, number_elected=1, votes_allowed=1, name="", ballot_selections=[ SelectionDescription( "0@A.com-selection", 0, "0@A.com", ), # Note the selection description is the same as the first sequence element SelectionDescription( "0@A.com-selection", 1, "0@A.com", ), ], ) keypair = elgamal_keypair_from_secret(TWO_MOD_Q) seed = ONE_MOD_Q # Bypass checking the validity of the description data = ballot_factory.get_random_contest_from( description, Random(0), suppress_validity_check=True ) placeholders = generate_placeholder_selections_from( description, description.number_elected ) description_with_placeholders = contest_description_with_placeholders_from( description, placeholders ) # Act subject = encrypt_contest( data, description_with_placeholders, keypair.public_key, ONE_MOD_Q, seed ) self.assertIsNone(subject) def test_encrypt_ballot_simple_succeeds(self): # Arrange keypair = elgamal_keypair_from_secret(int_to_q(2)) manifest = election_factory.get_fake_manifest() internal_manifest, context = election_factory.get_fake_ciphertext_election( manifest, keypair.public_key ) nonce_seed = TWO_MOD_Q # TODO: Ballot Factory subject = election_factory.get_fake_ballot(internal_manifest) self.assertTrue(subject.is_valid(internal_manifest.ballot_styles[0].object_id)) # Act result = encrypt_ballot(subject, internal_manifest, context, SEED) result_from_seed = encrypt_ballot( subject, internal_manifest, context, SEED, nonce_seed, should_verify_proofs=True, ) # Assert self.assertIsNotNone(result) self.assertIsNotNone(result.code) self.assertIsNotNone(result_from_seed) self.assertTrue( result.is_valid_encryption( internal_manifest.manifest_hash, keypair.public_key, context.crypto_extended_base_hash, ) ) self.assertTrue( result_from_seed.is_valid_encryption( internal_manifest.manifest_hash, keypair.public_key, context.crypto_extended_base_hash, ) ) def test_encrypt_ballot_with_composer_succeeds(self): # Arrange keypair = elgamal_keypair_from_secret(int_to_q(2)) manifest = election_factory.get_fake_manifest() internal_manifest, context = election_factory.get_fake_ciphertext_election( manifest, keypair.public_key ) data = election_factory.get_fake_ballot(internal_manifest) self.assertTrue(data.is_valid(internal_manifest.ballot_styles[0].object_id)) device = election_factory.get_encryption_device() subject = EncryptionMediator(internal_manifest, context, device) # Act result = subject.encrypt(data) # Assert self.assertIsNotNone(result) self.assertTrue( result.is_valid_encryption( internal_manifest.manifest_hash, keypair.public_key, context.crypto_extended_base_hash, ) ) def test_encrypt_simple_ballot_from_file_with_composer_succeeds(self): # Arrange keypair = elgamal_keypair_from_secret(int_to_q(2)) manifest = election_factory.get_simple_manifest_from_file() internal_manifest, context = election_factory.get_fake_ciphertext_election( manifest, keypair.public_key ) data = ballot_factory.get_simple_ballot_from_file() self.assertTrue(data.is_valid(internal_manifest.ballot_styles[0].object_id)) device = EncryptionDevice(12345, 23456, 34567, "Location") subject = EncryptionMediator(internal_manifest, context, device) # Act result = subject.encrypt(data) # Assert self.assertIsNotNone(result) self.assertEqual(data.object_id, result.object_id) self.assertTrue( result.is_valid_encryption( internal_manifest.manifest_hash, keypair.public_key, context.crypto_extended_base_hash, ) ) def test_encrypt_simple_ballot_from_files_succeeds(self) -> None: # Arrange keypair = elgamal_keypair_from_secret(int_to_q(2)) manifest = election_factory.get_simple_manifest_from_file() internal_manifest, context = election_factory.get_fake_ciphertext_election( manifest, keypair.public_key ) device = EncryptionDevice(12345, 23456, 34567, "Location") ballot = ballot_factory.get_simple_ballot_from_file() self.assertTrue(ballot.is_valid(internal_manifest.ballot_styles[0].object_id)) # Act ciphertext = encrypt_ballot( ballot, internal_manifest, context, device.get_hash(), TWO_MOD_Q, should_verify_proofs=True, ) # Assert self.assertIsNotNone(ciphertext) self.assertEqual(ballot.object_id, ciphertext.object_id) self.assertTrue( ciphertext.is_valid_encryption( internal_manifest.manifest_hash, keypair.public_key, context.crypto_extended_base_hash, ) ) @settings( deadline=timedelta(milliseconds=4000), suppress_health_check=[HealthCheck.too_slow], max_examples=10, ) @given(elgamal_keypairs()) def test_encrypt_ballot_with_derivative_nonces_regenerates_valid_proofs( self, keypair: ElGamalKeyPair ): """ This test verifies that we can regenerate the contest and selection proofs from the cached nonce values """ # TODO: Hypothesis test instead # Arrange manifest = election_factory.get_simple_manifest_from_file() internal_manifest, context = election_factory.get_fake_ciphertext_election( manifest, keypair.public_key ) data = ballot_factory.get_simple_ballot_from_file() self.assertTrue(data.is_valid(internal_manifest.ballot_styles[0].object_id)) device = election_factory.get_encryption_device() subject = EncryptionMediator(internal_manifest, context, device) # Act result = subject.encrypt(data) self.assertTrue( result.is_valid_encryption( internal_manifest.manifest_hash, keypair.public_key, context.crypto_extended_base_hash, ) ) # Assert for contest in result.contests: # Find the contest description contest_description = list( filter( lambda i, c=contest: i.object_id == c.object_id, internal_manifest.contests, ) )[0] # Homomorpically accumulate the selection encryptions elgamal_accumulation = elgamal_add( *[selection.ciphertext for selection in contest.ballot_selections] ) # accumulate the selection nonce's aggregate_nonce = add_q( *[selection.nonce for selection in contest.ballot_selections] ) regenerated_constant = make_constant_chaum_pedersen( elgamal_accumulation, contest_description.number_elected, aggregate_nonce, keypair.public_key, add_q(contest.nonce, TWO_MOD_Q), context.crypto_extended_base_hash, ) self.assertTrue( regenerated_constant.is_valid( elgamal_accumulation, keypair.public_key, context.crypto_extended_base_hash, ) ) for selection in contest.ballot_selections: # Since we know the nonce, we can decrypt the plaintext representation = selection.ciphertext.decrypt_known_nonce( keypair.public_key, selection.nonce ) # one could also decrypt with the secret key: # representation = selection.message.decrypt(keypair.secret_key) regenerated_disjuctive = make_disjunctive_chaum_pedersen( selection.ciphertext, selection.nonce, keypair.public_key, context.crypto_extended_base_hash, add_q(selection.nonce, TWO_MOD_Q), representation, ) self.assertTrue( regenerated_disjuctive.is_valid( selection.ciphertext, keypair.public_key, context.crypto_extended_base_hash, ) ) def test_encrypt_ballot_with_verify_proofs_false_passed_on(self): """ This test is for https://github.com/microsoft/electionguard-python/issues/459 """ with ( patch("electionguard.encrypt.encrypt_contest") as patched_contest, patch("electionguard.encrypt.encrypt_selection") as patched_selection, ): # Arrange keypair = elgamal_keypair_from_secret(int_to_q(2)) manifest = election_factory.get_fake_manifest() internal_manifest, context = election_factory.get_fake_ciphertext_election( manifest, keypair.public_key ) subject = election_factory.get_fake_ballot(internal_manifest) self.assertTrue( subject.is_valid(internal_manifest.ballot_styles[0].object_id) ) patched_contest.side_effect = encrypt_contest patched_selection.side_effect = encrypt_selection # Act encrypt_ballot( subject, internal_manifest, context, SEED, should_verify_proofs=False ) # Assert for call in patched_contest.call_args_list: self.assertFalse(call.kwargs.get("should_verify_proofs")) for call in patched_selection.call_args_list: self.assertFalse(call.kwargs.get("should_verify_proofs")) ================================================ FILE: tests/property/test_encrypt_hypotheses.py ================================================ from datetime import timedelta from typing import List, Dict from hypothesis import given, HealthCheck, settings, Phase from hypothesis.strategies import integers from tests.base_test_case import BaseTestCase from electionguard.ballot import CiphertextBallot from electionguard.decrypt_with_secrets import decrypt_ballot_with_secret from electionguard.elgamal import ElGamalCiphertext, elgamal_encrypt, elgamal_add from electionguard.encrypt import encrypt_ballot from electionguard.group import ElementModQ from electionguard.manifest import Manifest from electionguard.nonces import Nonces from electionguard_tools.strategies.election import ( election_descriptions, elections_and_ballots, ElectionsAndBallotsTupleType, ) from electionguard_tools.factories.election_factory import ElectionFactory from electionguard_tools.strategies.group import elements_mod_q from electionguard_tools.helpers.tally_accumulate import accumulate_plaintext_ballots SEED = ElectionFactory.get_encryption_device().get_hash() class TestElections(BaseTestCase): """Election hypothesis encryption tests""" @settings( deadline=timedelta(milliseconds=2000), suppress_health_check=[HealthCheck.too_slow], max_examples=10, ) @given(election_descriptions()) def test_generators_yield_valid_output(self, manifest: Manifest): """ Tests that our Hypothesis election strategies generate "valid" output, also exercises the full stack of `is_valid` methods. """ self.assertTrue(manifest.is_valid()) @settings( deadline=timedelta(milliseconds=10000), suppress_health_check=[HealthCheck.too_slow], max_examples=5, # disabling the "shrink" phase, because it runs very slowly phases=[Phase.explicit, Phase.reuse, Phase.generate, Phase.target], ) @given( integers(1, 3).flatmap(lambda n: elections_and_ballots(n)), elements_mod_q(), ) def test_accumulation_encryption_decryption( self, everything: ElectionsAndBallotsTupleType, nonce: ElementModQ, ): """ Tests that decryption is the inverse of encryption over arbitrarily generated elections and ballots. This test uses an abitrarily generated dataset with a single public-private keypair for the election encryption context. It also manually verifies that homomorphic accumulation works as expected. """ # Arrange ( _election_description, internal_manifest, ballots, secret_key, context, ) = everything # Tally the plaintext ballots for comparison later plaintext_tallies = accumulate_plaintext_ballots(ballots) num_ballots = len(ballots) num_contests = len(internal_manifest.contests) zero_nonce, *nonces = Nonces(nonce)[: num_ballots + 1] self.assertEqual(len(nonces), num_ballots) self.assertTrue(len(internal_manifest.contests) > 0) # Generate a valid encryption of zero encrypted_zero = elgamal_encrypt(0, zero_nonce, context.elgamal_public_key) # Act encrypted_ballots = [] # encrypt each ballot for i in range(num_ballots): encrypted_ballot = encrypt_ballot( ballots[i], internal_manifest, context, SEED, nonces[i], should_verify_proofs=True, ) encrypted_ballots.append(encrypted_ballot) # sanity check the encryption self.assertIsNotNone(encrypted_ballot) self.assertEqual(num_contests, len(encrypted_ballot.contests)) # decrypt the ballot with secret and verify it matches the plaintext decrypted_ballot = decrypt_ballot_with_secret( encrypted_ballot, internal_manifest, context.crypto_extended_base_hash, context.elgamal_public_key, secret_key, ) self.assertEqual(ballots[i], decrypted_ballot) # homomorphically accumulate the encrypted ballot representations encrypted_tallies = _accumulate_encrypted_ballots( encrypted_zero, encrypted_ballots ) decrypted_tallies = {} for object_id, encrypted_tally in encrypted_tallies.items(): decrypted_tallies[object_id] = encrypted_tally.decrypt(secret_key) # loop through the contest descriptions and verify # the decrypted tallies match the plaintext tallies for contest in internal_manifest.contests: # Sanity check the generated data self.assertTrue(len(contest.ballot_selections) > 0) self.assertTrue(len(contest.placeholder_selections) > 0) decrypted_selection_tallies = [ decrypted_tallies[selection.object_id] for selection in contest.ballot_selections ] decrypted_placeholder_tallies = [ decrypted_tallies[placeholder.object_id] for placeholder in contest.placeholder_selections ] plaintext_tally_values = [ plaintext_tallies[selection.object_id] for selection in contest.ballot_selections ] # verify the plaintext tallies match the decrypted tallies self.assertEqual(decrypted_selection_tallies, plaintext_tally_values) # validate the right number of selections including placeholders across all ballots self.assertEqual( contest.number_elected * num_ballots, sum(decrypted_selection_tallies) + sum(decrypted_placeholder_tallies), ) def _accumulate_encrypted_ballots( encrypted_zero: ElGamalCiphertext, ballots: List[CiphertextBallot] ) -> Dict[str, ElGamalCiphertext]: """ Internal helper function for testing: takes a list of encrypted ballots as input, digs into all of the individual selections and then accumulates them, using their `object_id` fields as keys. This function only knows what to do with `n_of_m` elections. It's not a general-purpose tallying mechanism for other election types. Note that the output will include both "normal" and "placeholder" selections. :param encrypted_zero: an encrypted zero, used for the accumulation :param ballots: a list of encrypted ballots :return: a dict from selection object_id's to `ElGamalCiphertext` totals """ tally: Dict[str, ElGamalCiphertext] = {} for ballot in ballots: for contest in ballot.contests: for selection in contest.ballot_selections: desc_id = ( selection.object_id ) # this should be the same as in the PlaintextBallot! if desc_id not in tally: tally[desc_id] = encrypted_zero tally[desc_id] = elgamal_add(tally[desc_id], selection.ciphertext) return tally ================================================ FILE: tests/property/test_group.py ================================================ from typing import Optional from hypothesis import given from tests.base_test_case import BaseTestCase from electionguard.constants import ( get_small_prime, get_large_prime, get_generator, get_cofactor, ) from electionguard.group import ( ElementModP, ElementModQ, a_minus_b_q, mult_inv_p, ONE_MOD_P, mult_p, ZERO_MOD_P, ONE_MOD_Q, g_pow_p, ZERO_MOD_Q, int_to_p, int_to_q, add_q, div_q, div_p, a_plus_bc_q, ) from electionguard.utils import ( flatmap_optional, get_or_else_optional, match_optional, get_optional, ) from electionguard_tools.strategies.group import ( elements_mod_p_no_zero, elements_mod_p, elements_mod_q, elements_mod_q_no_zero, ) class TestEquality(BaseTestCase): """Math equality tests""" @given(elements_mod_q(), elements_mod_q()) def test_p_not_equal_to_q(self, q: ElementModQ, q2: ElementModQ) -> None: i = int(q) i2 = int(q2) p = ElementModP(q) p2 = ElementModP(q2) # same value should imply they're equal self.assertEqual(p, q) self.assertEqual(q, p) self.assertEqual(p, i) self.assertEqual(q, i) if q != q2: # these are genuinely different numbers self.assertNotEqual(q, q2) self.assertNotEqual(p, p2) self.assertNotEqual(q, p2) self.assertNotEqual(p, q2) self.assertNotEqual(q, i2) self.assertNotEqual(p, i2) self.assertNotEqual(q2, i) self.assertNotEqual(p2, i) # of course, we're going to make sure that a number is equal to itself self.assertEqual(p, p) self.assertEqual(q, q) class TestModularArithmetic(BaseTestCase): """Math Modular Arithmetic tests""" @given(elements_mod_q()) def test_add_q(self, q: ElementModQ) -> None: as_int = add_q(q, 1) as_elem = add_q(q, ElementModQ(1)) self.assertEqual(as_int, as_elem) @given(elements_mod_q()) def test_a_plus_bc_q(self, q: ElementModQ) -> None: as_int = a_plus_bc_q(q, 1, 1) as_elem = a_plus_bc_q(q, ElementModQ(1), ElementModQ(1)) self.assertEqual(as_int, as_elem) @given(elements_mod_q()) def test_a_minus_b_q(self, q: ElementModQ) -> None: as_int = a_minus_b_q(q, 1) as_elem = a_minus_b_q(q, ElementModQ(1)) self.assertEqual(as_int, as_elem) @given(elements_mod_q()) def test_div_q(self, q: ElementModQ) -> None: as_int = div_q(q, 1) as_elem = div_q(q, ElementModQ(1)) self.assertEqual(as_int, as_elem) @given(elements_mod_p()) def test_div_p(self, p: ElementModQ) -> None: as_int = div_p(p, 1) as_elem = div_p(p, ElementModP(1)) self.assertEqual(as_int, as_elem) def test_no_mult_inv_of_zero(self) -> None: self.assertRaises(Exception, mult_inv_p, ZERO_MOD_P) @given(elements_mod_p_no_zero()) def test_mult_inverses(self, elem: ElementModP) -> None: inv = mult_inv_p(elem) self.assertEqual(mult_p(elem, inv), ONE_MOD_P) @given(elements_mod_p()) def test_mult_identity(self, elem: ElementModP) -> None: self.assertEqual(elem, mult_p(elem)) def test_mult_noargs(self) -> None: self.assertEqual(ONE_MOD_P, mult_p()) def test_add_noargs(self) -> None: self.assertEqual(ZERO_MOD_Q, add_q()) def test_properties_for_constants(self) -> None: self.assertNotEqual(get_generator(), 1) self.assertEqual( (get_cofactor() * get_small_prime()) % get_large_prime(), get_large_prime() - 1, ) self.assertLess(get_small_prime(), get_large_prime()) self.assertLess(get_generator(), get_large_prime()) self.assertLess(get_cofactor(), get_large_prime()) def test_simple_powers(self) -> None: gp = int_to_p(get_generator()) self.assertEqual(gp, g_pow_p(ONE_MOD_Q)) self.assertEqual(ONE_MOD_P, g_pow_p(ZERO_MOD_Q)) @given(elements_mod_q()) def test_in_bounds_q(self, q: ElementModQ) -> None: self.assertTrue(q.is_in_bounds()) too_big = q.value + get_small_prime() too_small = q.value - get_small_prime() self.assertFalse(ElementModQ(too_big, False).is_in_bounds()) self.assertFalse(ElementModQ(too_small, False).is_in_bounds()) self.assertEqual(None, int_to_q(too_big)) self.assertEqual(None, int_to_q(too_small)) with self.assertRaises(OverflowError): ElementModQ(too_big) with self.assertRaises(OverflowError): ElementModQ(too_small) @given(elements_mod_p()) def test_in_bounds_p(self, p: ElementModP) -> None: self.assertTrue(p.is_in_bounds()) too_big = p.value + get_large_prime() too_small = p.value - get_large_prime() self.assertFalse(ElementModP(too_big, False).is_in_bounds()) self.assertFalse(ElementModP(too_small, False).is_in_bounds()) self.assertEqual(None, int_to_p(too_big)) self.assertEqual(None, int_to_p(too_small)) with self.assertRaises(OverflowError): ElementModP(too_big) with self.assertRaises(OverflowError): ElementModP(too_small) @given(elements_mod_q_no_zero()) def test_in_bounds_q_no_zero(self, q: ElementModQ): self.assertTrue(q.is_in_bounds_no_zero()) self.assertFalse(ZERO_MOD_Q.is_in_bounds_no_zero()) self.assertFalse( ElementModQ(q.value + get_small_prime(), False).is_in_bounds_no_zero() ) self.assertFalse( ElementModQ(q.value - get_small_prime(), False).is_in_bounds_no_zero() ) @given(elements_mod_p_no_zero()) def test_in_bounds_p_no_zero(self, p: ElementModP) -> None: self.assertTrue(p.is_in_bounds_no_zero()) self.assertFalse(ZERO_MOD_P.is_in_bounds_no_zero()) self.assertFalse( ElementModP(p.value + get_large_prime(), False).is_in_bounds_no_zero() ) self.assertFalse( ElementModP(p.value - get_large_prime(), False).is_in_bounds_no_zero() ) @given(elements_mod_q()) def test_large_values_rejected_by_int_to_q(self, q: ElementModQ) -> None: oversize = q.value + get_small_prime() self.assertEqual(None, int_to_q(oversize)) class TestOptionalFunctions(BaseTestCase): """Math Optional Functions tests""" def test_unwrap(self) -> None: good: Optional[int] = 3 bad: Optional[int] = None self.assertEqual(get_optional(good), 3) self.assertRaises(Exception, get_optional, bad) def test_match(self) -> None: good: Optional[int] = 3 bad: Optional[int] = None self.assertEqual(5, match_optional(good, lambda: 1, lambda x: x + 2)) self.assertEqual(1, match_optional(bad, lambda: 1, lambda x: x + 2)) def test_get_or_else(self) -> None: good: Optional[int] = 3 bad: Optional[int] = None self.assertEqual(3, get_or_else_optional(good, 5)) self.assertEqual(5, get_or_else_optional(bad, 5)) def test_flatmap(self) -> None: good: Optional[int] = 3 bad: Optional[int] = None self.assertEqual(5, get_optional(flatmap_optional(good, lambda x: x + 2))) self.assertIsNone(flatmap_optional(bad, lambda x: x + 2)) ================================================ FILE: tests/property/test_hash.py ================================================ from typing import List, Optional from hypothesis import given from hypothesis.strategies import integers from tests.base_test_case import BaseTestCase from electionguard.big_integer import BigInteger from electionguard.group import ElementModP, ElementModQ from electionguard.hash import hash_elems from electionguard_tools.strategies.group import elements_mod_p, elements_mod_q class TestHash(BaseTestCase): """Hash tests""" @given(elements_mod_p(), elements_mod_q()) def test_same_answer_twice_in_a_row(self, a: ElementModQ, b: ElementModQ): # if this doesn't work, then our hash function isn't a function h1 = hash_elems(a, b) h2 = hash_elems(a, b) self.assertEqual(h1, h2) @given(elements_mod_q(), elements_mod_q()) def test_basic_hash_properties(self, a: ElementModQ, b: ElementModQ): ha = hash_elems(a) hb = hash_elems(b) if a == b: self.assertEqual(ha, hb) if ha != hb: self.assertNotEqual(a, b) @given(elements_mod_p(), integers(min_value=0, max_value=10)) def test_hash_of_big_integer_with_leading_zero_bytes( self, input: ElementModP, multiplier: int ) -> None: """Test hashing of larger integers with leading zero bytes""" # Arrange. zero_byte = "00" input_hash = hash_elems(input) leading_zeroes = zero_byte * multiplier + input.to_hex() # Act. leading_zeroes_big_int = BigInteger(leading_zeroes) leading_zeroes_hash = hash_elems(leading_zeroes_big_int) # Assert. self.assertEqual(input, leading_zeroes_big_int) self.assertEqual(input_hash, leading_zeroes_hash) @given(elements_mod_p()) def test_hash_of_big_integer_with_single_leading_zero( self, input: ElementModP ) -> None: """Test hashing of big integer with a single leading zero creating an invalid hex byte reprsentation.""" # Arrange. invalid_hex = "0" + input.to_hex() input_hash = hash_elems(input) # Act. invalid_hex_big_int = BigInteger(invalid_hex) invalid_hex_hash = hash_elems(invalid_hex_big_int) # Assert. self.assertEqual(input, invalid_hex_big_int) self.assertEqual(input_hash, invalid_hex_hash) def test_hash_for_zero_number_is_zero_string(self): self.assertEqual(hash_elems(0), hash_elems("0")) def test_hash_for_non_zero_number_string_same_as_explicit_number(self): self.assertEqual(hash_elems(1), hash_elems("1")) def test_different_strings_casing_not_the_same_hash(self): self.assertNotEqual( hash_elems("Welcome To ElectionGuard"), hash_elems("welcome to electionguard"), ) def test_hash_for_none_same_as_null_string(self): self.assertEqual(hash_elems(None), hash_elems("null")) def test_hash_of_save_values_in_list_are_same_hash(self): self.assertEqual(hash_elems(["0", "0"]), hash_elems(["0", "0"])) def test_hash_null_equivalents(self): null_list: Optional[List[str]] = None empty_list: List[str] = [] self.assertEqual(hash_elems(null_list), hash_elems(empty_list)) self.assertEqual(hash_elems(empty_list), hash_elems(None)) self.assertEqual(hash_elems(empty_list), hash_elems("null")) def test_hash_not_null_equivalents(self): self.assertNotEqual(hash_elems(None), hash_elems("")) self.assertNotEqual(hash_elems(None), hash_elems(0)) def test_hash_value_from_nested_list_and_result_of_hashed_list_by_taking_the_hex( self, ): nested_hash = hash_elems(["0", "1"], "3") non_nested_1 = hash_elems("0", "1") non_nested_2 = hash_elems(non_nested_1.to_hex(), "3") self.assertNotEqual(nested_hash, non_nested_1) self.assertEqual(nested_hash, non_nested_2) ================================================ FILE: tests/property/test_nonces.py ================================================ from typing import List from hypothesis import given, assume from hypothesis.strategies import integers from tests.base_test_case import BaseTestCase from electionguard.group import ElementModQ from electionguard.nonces import Nonces from electionguard_tools.strategies.group import elements_mod_q class TestNonces(BaseTestCase): """Nonces tests""" @given(elements_mod_q()) def test_nonces_iterable(self, seed: ElementModQ): n = Nonces(seed) i = iter(n) q0 = next(i) q1 = next(i) self.assertTrue(q0 != q1) @given(elements_mod_q(), integers(min_value=0, max_value=1000000)) def test_nonces_deterministic(self, seed: ElementModQ, i: int): n1 = Nonces(seed) n2 = Nonces(seed) self.assertEqual(n1[i], n2[i]) @given( elements_mod_q(), elements_mod_q(), ) def test_nonces_seed_matters(self, seed1: ElementModQ, seed2: ElementModQ): assume(seed1 != seed2) n1 = Nonces(seed1) # pylint: disable=unreachable n2 = Nonces(seed2) # pylint: disable=unreachable self.assertNotEqual(n1[0], n2[0]) @given(elements_mod_q()) def test_nonces_with_slices(self, seed: ElementModQ): n = Nonces(seed) count: int = 0 l: List[ElementModQ] = [] for i in iter(n): count += 1 l.append(i) if count == 10: break self.assertEqual(len(l), 10) l2 = Nonces(seed)[0:10] self.assertEqual(len(l2), 10) self.assertEqual(l, l2) def test_nonces_type_errors(self): n = Nonces(ElementModQ(3)) self.assertRaises(TypeError, len, n) self.assertRaises(TypeError, lambda: n[1:]) self.assertRaises(TypeError, lambda: n.get_with_headers(-1)) ================================================ FILE: tests/property/test_schnorr.py ================================================ from hypothesis import given, assume from tests.base_test_case import BaseTestCase from electionguard.constants import get_large_prime from electionguard.elgamal import ElGamalKeyPair, elgamal_keypair_from_secret from electionguard.group import ( ElementModQ, ElementModP, ZERO_MOD_P, TWO_MOD_Q, ONE_MOD_Q, ) from electionguard.schnorr import ( make_schnorr_proof, SchnorrProof, ) from electionguard.utils import get_optional from electionguard_tools.strategies.elgamal import elgamal_keypairs from electionguard_tools.strategies.group import ( elements_mod_q, elements_mod_p_no_zero, elements_mod_p, ) class TestSchnorr(BaseTestCase): """Schnorr tests""" def test_schnorr_proofs_simple(self) -> None: # doesn't get any simpler than this keypair = get_optional(elgamal_keypair_from_secret(TWO_MOD_Q)) nonce = ONE_MOD_Q proof = make_schnorr_proof(keypair, nonce) self.assertTrue(proof.is_valid()) @given(elgamal_keypairs(), elements_mod_q()) def test_schnorr_proofs_valid( self, keypair: ElGamalKeyPair, nonce: ElementModQ ) -> None: proof = make_schnorr_proof(keypair, nonce) self.assertTrue(proof.is_valid()) # Now, we introduce errors in the proofs and make sure that they fail to verify @given(elgamal_keypairs(), elements_mod_q(), elements_mod_q()) def test_schnorr_proofs_invalid_u( self, keypair: ElGamalKeyPair, nonce: ElementModQ, other: ElementModQ ) -> None: proof = make_schnorr_proof(keypair, nonce) assume(other != proof.response) proof2 = SchnorrProof( # pylint: disable=unreachable proof.public_key, proof.commitment, proof.challenge, other ) self.assertFalse(proof2.is_valid()) @given(elgamal_keypairs(), elements_mod_q(), elements_mod_p()) def test_schnorr_proofs_invalid_h( self, keypair: ElGamalKeyPair, nonce: ElementModQ, other: ElementModP ) -> None: proof = make_schnorr_proof(keypair, nonce) assume(other != proof.commitment) proof_bad = SchnorrProof( # pylint: disable=unreachable proof.public_key, other, proof.challenge, proof.response ) self.assertFalse(proof_bad.is_valid()) @given(elgamal_keypairs(), elements_mod_q(), elements_mod_p_no_zero()) def test_schnorr_proofs_invalid_public_key( self, keypair: ElGamalKeyPair, nonce: ElementModQ, other: ElementModP ) -> None: proof = make_schnorr_proof(keypair, nonce) assume(other != proof.public_key) proof2 = SchnorrProof( # pylint: disable=unreachable other, proof.commitment, proof.challenge, proof.response ) self.assertFalse(proof2.is_valid()) @given(elgamal_keypairs(), elements_mod_q()) def test_schnorr_proofs_bounds_checking( self, keypair: ElGamalKeyPair, nonce: ElementModQ ) -> None: proof = make_schnorr_proof(keypair, nonce) proof2 = SchnorrProof( ZERO_MOD_P, proof.commitment, proof.challenge, proof.response ) proof3 = SchnorrProof( ElementModP(get_large_prime(), False), proof.commitment, proof.challenge, proof.response, ) self.assertFalse(proof2.is_valid()) self.assertFalse(proof3.is_valid()) ================================================ FILE: tests/property/test_tally.py ================================================ from datetime import timedelta from typing import Dict from hypothesis import given, HealthCheck, settings, Phase from hypothesis.strategies import integers from tests.base_test_case import BaseTestCase from electionguard.ballot import ( BallotBoxState, SubmittedBallot, ) from electionguard.ballot_box import cast_ballot, spoil_ballot from electionguard.data_store import DataStore from electionguard.elgamal import ElGamalSecretKey from electionguard.encrypt import encrypt_ballot from electionguard.group import ONE_MOD_Q from electionguard.tally import CiphertextTally, tally_ballots, tally_ballot from electionguard_tools.strategies.election import ( elections_and_ballots, ElectionsAndBallotsTupleType, ) from electionguard_tools.factories.election_factory import ElectionFactory from electionguard_tools.helpers.tally_accumulate import accumulate_plaintext_ballots class TestTally(BaseTestCase): """Tally tests""" @settings( deadline=timedelta(milliseconds=10000), suppress_health_check=[HealthCheck.too_slow], max_examples=3, # disabling the "shrink" phase, because it runs very slowly phases=[Phase.explicit, Phase.reuse, Phase.generate, Phase.target], ) @given(integers(2, 5).flatmap(lambda n: elections_and_ballots(n))) def test_tally_cast_ballots_accumulates_valid_tally( self, everything: ElectionsAndBallotsTupleType ): # Arrange ( _election_description, internal_manifest, ballots, secret_key, context, ) = everything # Tally the plaintext ballots for comparison later plaintext_tallies = accumulate_plaintext_ballots(ballots) # encrypt each ballot store = DataStore() encryption_seed = ElectionFactory.get_encryption_device().get_hash() for ballot in ballots: encrypted_ballot = encrypt_ballot( ballot, internal_manifest, context, encryption_seed, should_verify_proofs=True, ) encryption_seed = encrypted_ballot.code self.assertIsNotNone(encrypted_ballot) # add to the ballot store store.set( encrypted_ballot.object_id, cast_ballot(encrypted_ballot), ) # act result = tally_ballots(store, internal_manifest, context) self.assertIsNotNone(result) # Assert decrypted_tallies = self._decrypt_with_secret(result, secret_key) self.assertEqual(plaintext_tallies, decrypted_tallies) @settings( deadline=timedelta(milliseconds=10000), suppress_health_check=[HealthCheck.too_slow], max_examples=3, # disabling the "shrink" phase, because it runs very slowly phases=[Phase.explicit, Phase.reuse, Phase.generate, Phase.target], ) @given(integers(1, 3).flatmap(lambda n: elections_and_ballots(n))) def test_tally_spoiled_ballots_accumulates_valid_tally( self, everything: ElectionsAndBallotsTupleType ): # Arrange ( _election_description, internal_manifest, ballots, secret_key, context, ) = everything # Tally the plaintext ballots for comparison later plaintext_tallies = accumulate_plaintext_ballots(ballots) # encrypt each ballot store = DataStore() encryption_seed = ElectionFactory.get_encryption_device().get_hash() for ballot in ballots: encrypted_ballot = encrypt_ballot( ballot, internal_manifest, context, encryption_seed, should_verify_proofs=True, ) encryption_seed = encrypted_ballot.code self.assertIsNotNone(encrypted_ballot) # add to the ballot store store.set( encrypted_ballot.object_id, spoil_ballot(encrypted_ballot), ) # act tally = tally_ballots(store, internal_manifest, context) self.assertIsNotNone(tally) # Assert decrypted_tallies = self._decrypt_with_secret(tally, secret_key) self.assertCountEqual(plaintext_tallies, decrypted_tallies) for value in decrypted_tallies.values(): self.assertEqual(0, value) self.assertEqual(len(ballots), tally.spoiled()) @settings( deadline=timedelta(milliseconds=10000), suppress_health_check=[HealthCheck.too_slow], max_examples=3, # disabling the "shrink" phase, because it runs very slowly phases=[Phase.explicit, Phase.reuse, Phase.generate, Phase.target], ) @given(integers(1, 3).flatmap(lambda n: elections_and_ballots(n))) def test_tally_ballot_invalid_input_fails( self, everything: ElectionsAndBallotsTupleType ): # Arrange ( _election_description, internal_manifest, ballots, _secret_key, context, ) = everything # encrypt each ballot store = DataStore() encryption_seed = ElectionFactory.get_encryption_device().get_hash() for ballot in ballots: encrypted_ballot = encrypt_ballot( ballot, internal_manifest, context, encryption_seed ) encryption_seed = encrypted_ballot.code self.assertIsNotNone(encrypted_ballot) # add to the ballot store store.set( encrypted_ballot.object_id, cast_ballot(encrypted_ballot), ) tally = CiphertextTally("my-tally", internal_manifest, context) # act cached_ballots = store.all() first_ballot = cached_ballots[0] first_ballot.state = BallotBoxState.UNKNOWN # verify an UNKNOWN state ballot fails self.assertIsNone(tally_ballot(first_ballot, tally)) self.assertFalse(tally.append(first_ballot, True)) # cast a ballot first_ballot.state = BallotBoxState.CAST self.assertTrue(tally.append(first_ballot, False)) # try to append a spoiled ballot first_ballot.state = BallotBoxState.SPOILED self.assertFalse(tally.append(first_ballot, True)) # Verify accumulation fails if the selection collection is empty if first_ballot.state == BallotBoxState.CAST: self.assertFalse( tally.contests[first_ballot.object_id].accumulate_contest([]) ) # pop the cast ballot tally.cast_ballot_ids.pop() # reset to cast first_ballot.state = BallotBoxState.CAST self.assertTrue( self._cannot_erroneously_mutate_state( tally, first_ballot, BallotBoxState.CAST ) ) self.assertTrue( self._cannot_erroneously_mutate_state( tally, first_ballot, BallotBoxState.SPOILED ) ) self.assertTrue( self._cannot_erroneously_mutate_state( tally, first_ballot, BallotBoxState.UNKNOWN ) ) # verify a cast ballot cannot be added twice first_ballot.state = BallotBoxState.CAST self.assertTrue(tally.append(first_ballot, True)) self.assertFalse(tally.append(first_ballot, False)) # verify an already submitted ballot cannot be changed or readded first_ballot.state = BallotBoxState.SPOILED self.assertFalse(tally.append(first_ballot, True)) @staticmethod def _decrypt_with_secret( tally: CiphertextTally, secret_key: ElGamalSecretKey ) -> Dict[str, int]: """ Demonstrates how to decrypt a tally with a known secret key """ plaintext_selections: Dict[str, int] = {} for _, contest in tally.contests.items(): for object_id, selection in contest.selections.items(): plaintext_tally = selection.ciphertext.decrypt(secret_key) plaintext_selections[object_id] = plaintext_tally return plaintext_selections def _cannot_erroneously_mutate_state( self, tally: CiphertextTally, ballot: SubmittedBallot, state_to_test: BallotBoxState, ) -> bool: input_state = ballot.state ballot.state = state_to_test # remove the first selection first_contest = ballot.contests[0] first_selection = first_contest.ballot_selections[0] ballot.contests[0].ballot_selections.remove(first_selection) self.assertIsNone(tally_ballot(ballot, tally)) self.assertFalse(tally.append(ballot, True)) # Verify accumulation fails if the selection count does not match if ballot.state == BallotBoxState.CAST: first_tally = tally.contests[first_contest.object_id] self.assertFalse( first_tally.accumulate_contest(ballot.contests[0].ballot_selections) ) # pylint: disable=protected-access _key, bad_accumulation = first_tally._accumulate_selections( first_selection.object_id, first_tally.selections[first_selection.object_id], ballot.contests[0].ballot_selections, ) self.assertIsNone(bad_accumulation) ballot.contests[0].ballot_selections.insert(0, first_selection) # modify the contest description hash first_contest_hash = ballot.contests[0].description_hash ballot.contests[0].description_hash = ONE_MOD_Q self.assertIsNone(tally_ballot(ballot, tally)) self.assertFalse(tally.append(ballot, True)) ballot.contests[0].description_hash = first_contest_hash # modify a contest object id first_contest_object_id = ballot.contests[0].object_id ballot.contests[0].object_id = "a-bad-object-id" self.assertIsNone(tally_ballot(ballot, tally)) self.assertFalse(tally.append(ballot, True)) ballot.contests[0].object_id = first_contest_object_id # modify a selection object id first_contest_selection_object_id = ( ballot.contests[0].ballot_selections[0].object_id ) ballot.contests[0].ballot_selections[0].object_id = "another-bad-object-id" self.assertIsNone(tally_ballot(ballot, tally)) self.assertFalse(tally.append(ballot, True)) # Verify accumulation fails if the selection object id does not match if ballot.state == BallotBoxState.CAST: self.assertFalse( tally.contests[ballot.contests[0].object_id].accumulate_contest( ballot.contests[0].ballot_selections ) ) ballot.contests[0].ballot_selections[ 0 ].object_id = first_contest_selection_object_id # modify the ballot's hash first_ballot_hash = ballot.manifest_hash ballot.manifest_hash = ONE_MOD_Q self.assertIsNone(tally_ballot(ballot, tally)) self.assertFalse(tally.append(ballot, True)) ballot.manifest_hash = first_ballot_hash ballot.state = input_state return True ================================================ FILE: tests/property/test_verify.py ================================================ # pylint: disable=protected-access from datetime import timedelta from typing import Dict from hypothesis import given, HealthCheck, settings, Phase from hypothesis.strategies import integers from tests.base_test_case import BaseTestCase from electionguard.ballot_box import spoil_ballot from electionguard.data_store import DataStore from electionguard.decryption import compute_decryption_share from electionguard.decryption_share import DecryptionShare from electionguard.decrypt_with_shares import decrypt_tally from electionguard.elgamal import ElGamalKeyPair from electionguard.encrypt import EncryptionMediator, encrypt_ballot from electionguard.key_ceremony import CeremonyDetails from electionguard.key_ceremony_mediator import KeyCeremonyMediator from electionguard.tally import tally_ballots from electionguard.type import GuardianId from electionguard.utils import get_optional from electionguard_verify.verify import ( verify_ballot, verify_decryption, verify_aggregation, ) import electionguard_tools.factories.ballot_factory as BallotFactory import electionguard_tools.factories.election_factory as ElectionFactory from electionguard_tools.strategies.election import ( elections_and_ballots, ElectionsAndBallotsTupleType, ) from electionguard_tools.strategies.elgamal import elgamal_keypairs from electionguard_tools.helpers.key_ceremony_orchestrator import ( KeyCeremonyOrchestrator, ) from electionguard_tools.helpers.election_builder import ElectionBuilder election_factory = ElectionFactory.ElectionFactory() ballot_factory = BallotFactory.BallotFactory() class TestVerify(BaseTestCase): """Test ballot verification""" @settings( deadline=timedelta(milliseconds=2000), suppress_health_check=[HealthCheck.too_slow], max_examples=10, # disabling the "shrink" phase, because it runs very slowly phases=[Phase.explicit, Phase.reuse, Phase.generate, Phase.target], ) @given(elgamal_keypairs()) def test_verify_ballot(self, keypair: ElGamalKeyPair): # Arrange manifest = election_factory.get_simple_manifest_from_file() internal_manifest, context = election_factory.get_fake_ciphertext_election( manifest, keypair.public_key ) data = ballot_factory.get_simple_ballot_from_file() device = election_factory.get_encryption_device() operator = EncryptionMediator(internal_manifest, context, device) encrypted_ballot = operator.encrypt(data) self.assertIsNotNone(encrypted_ballot) # Act verification = verify_ballot(encrypted_ballot, manifest, context) # Assert self.assertIsNotNone(verification) self.assertTrue(verification.verified) def test_verify_decryption(self): # Arrange NUMBER_OF_GUARDIANS = 3 QUORUM = 2 CEREMONY_DETAILS = CeremonyDetails(NUMBER_OF_GUARDIANS, QUORUM) key_ceremony_mediator = KeyCeremonyMediator( "key_ceremony_mediator_mediator", CEREMONY_DETAILS ) guardians = KeyCeremonyOrchestrator.create_guardians(CEREMONY_DETAILS) KeyCeremonyOrchestrator.perform_full_ceremony(guardians, key_ceremony_mediator) joint_public_key = key_ceremony_mediator.publish_joint_key() election_public_keys = key_ceremony_mediator._election_public_keys # Setup the election manifest = election_factory.get_fake_manifest() builder = ElectionBuilder(NUMBER_OF_GUARDIANS, QUORUM, manifest) builder.set_public_key(joint_public_key.joint_public_key) builder.set_commitment_hash(joint_public_key.commitment_hash) internal_manifest, context = get_optional(builder.build()) # generate encrypted tally ballot_store = DataStore() ciphertext_tally = tally_ballots(ballot_store, internal_manifest, context) # precompute decryption shares for specific selection for the guardians shares: Dict[GuardianId, DecryptionShare] = { guardian.id: compute_decryption_share( guardian._election_keys, ciphertext_tally, context, ) for guardian in guardians } plaintext_tally = decrypt_tally( ciphertext_tally, shares, context.crypto_extended_base_hash, manifest ) # Act verification = verify_decryption(plaintext_tally, election_public_keys, context) # Assert self.assertIsNotNone(verification) self.assertTrue(verification.verified) @settings( deadline=timedelta(milliseconds=10000), suppress_health_check=[HealthCheck.too_slow], max_examples=10, # disabling the "shrink" phase, because it runs very slowly phases=[Phase.explicit, Phase.reuse, Phase.generate, Phase.target], ) @given(integers(1, 3).flatmap(lambda n: elections_and_ballots(n))) def test_verify_aggregation(self, election_details: ElectionsAndBallotsTupleType): # Arrange ( manifest, internal_manifest, ballots, _secret_key, context, ) = election_details # encrypt each ballot store = DataStore() encryption_seed = election_factory.get_encryption_device().get_hash() for ballot in ballots: encrypted_ballot = encrypt_ballot( ballot, internal_manifest, context, encryption_seed, should_verify_proofs=True, ) encryption_seed = encrypted_ballot.code self.assertIsNotNone(encrypted_ballot) # add to the ballot store store.set( encrypted_ballot.object_id, spoil_ballot(encrypted_ballot), ) # Generate Tally submitted_ballots = store.all() tally = tally_ballots(store, internal_manifest, context) self.assertIsNotNone(tally) # Act verification = verify_aggregation(submitted_ballots, tally, manifest, context) # Assert self.assertIsNotNone(verification) self.assertTrue(verification.verified) ================================================ FILE: tests/unit/__init__.py ================================================ ================================================ FILE: tests/unit/electionguard/__init__.py ================================================ ================================================ FILE: tests/unit/electionguard/test_ballot.py ================================================ from tests.base_test_case import BaseTestCase from electionguard.manifest import ( ContestDescriptionWithPlaceholders, SelectionDescription, VoteVariationType, ) from electionguard.encrypt import contest_from from electionguard.utils import NullVoteException, OverVoteException, UnderVoteException NUMBER_ELECTED = 2 def get_sample_contest_description() -> ContestDescriptionWithPlaceholders: ballot_selections = [ SelectionDescription("option-1-id", 1, "luke-skywalker-id"), SelectionDescription("option-2-id", 2, "darth-vader-id"), SelectionDescription("option-3-id", 3, "obi-wan-kenobi-id"), ] placeholder_selections = [ SelectionDescription("placeholder-1-id", 4, "placeholder-id"), SelectionDescription("placeholder-2-id", 5, "placeholder-id"), ] description = ContestDescriptionWithPlaceholders( "favorite-character-id", 1, "dagobah-id", VoteVariationType.n_of_m, NUMBER_ELECTED, None, "favorite-star-wars-character", ballot_selections, None, None, placeholder_selections, ) return description class TestBallot(BaseTestCase): """Ballot tests""" def test_contest_valid(self) -> None: # Arrange. contest_description = get_sample_contest_description() contest = contest_from(contest_description) # Add Votes for i in range(NUMBER_ELECTED): contest.ballot_selections[i].vote = 1 # Act & Assert. try: contest.valid(contest_description) except (NullVoteException, OverVoteException, UnderVoteException): self.fail("No exceptions should be thrown.") def test_contest_valid_with_null_vote(self) -> None: # Arrange. contest_description = get_sample_contest_description() null_vote = contest_from(contest_description) # Act & Assert. with self.assertRaises(NullVoteException): null_vote.valid(contest_description) def test_contest_valid_with_under_vote(self) -> None: # Arrange. contest_description = get_sample_contest_description() under_vote = contest_from(contest_description) # Add Votes for i in range(NUMBER_ELECTED - 1): under_vote.ballot_selections[i].vote = 1 # Act & Assert. with self.assertRaises(UnderVoteException): under_vote.valid(contest_description) def test_contest_valid_with_over_vote(self) -> None: # Arrange. contest_description = get_sample_contest_description() over_vote = contest_from(contest_description) # Add Votes for i in range(NUMBER_ELECTED + 1): over_vote.ballot_selections[i].vote = 1 # Act & Assert. with self.assertRaises(OverVoteException): over_vote.valid(contest_description) ================================================ FILE: tests/unit/electionguard/test_ballot_box.py ================================================ from tests.base_test_case import BaseTestCase from electionguard.ballot import BallotBoxState from electionguard.ballot_box import ( BallotBox, submit_ballot_to_box, cast_ballot, spoil_ballot, submit_ballot, ) from electionguard.data_store import DataStore from electionguard.elgamal import elgamal_keypair_from_secret from electionguard.encrypt import encrypt_ballot from electionguard.group import TWO_MOD_Q from electionguard.utils import get_optional import electionguard_tools.factories.election_factory as ElectionFactory class TestBallotBox(BaseTestCase): """Ballot box tests""" def setUp(self) -> None: """Setup ballot box tests by creating a mock ballot, manifest, and encryption context.""" election_factory = ElectionFactory.ElectionFactory() self.seed = election_factory.get_encryption_device().get_hash() keypair = get_optional(elgamal_keypair_from_secret(TWO_MOD_Q)) manifest = election_factory.get_fake_manifest() ( self.internal_manifest, self.context, ) = election_factory.get_fake_ciphertext_election(manifest, keypair.public_key) self.ballot = election_factory.get_fake_ballot(manifest) def test_ballot_box_cast_ballot(self) -> None: # Arrange encrypted_ballot = get_optional( encrypt_ballot( self.ballot, self.internal_manifest, self.context, self.seed, ) ) store: DataStore = DataStore() # Act ballot_box = BallotBox(self.internal_manifest, self.context, store) submitted_ballot = ballot_box.cast(encrypted_ballot) # Assert # Test returned ballot self.assertIsNotNone(submitted_ballot) self.assertEqual(submitted_ballot.state, BallotBoxState.CAST) # Test ballot in box ballot_in_box = store.get(encrypted_ballot.object_id) self.assertIsNotNone(ballot_in_box) self.assertEqual(ballot_in_box.state, BallotBoxState.CAST) self.assertEqual(ballot_in_box.object_id, submitted_ballot.object_id) # Test failure modes self.assertIsNone(ballot_box.cast(encrypted_ballot)) # cannot cast again self.assertIsNone( ballot_box.spoil(encrypted_ballot) ) # cannot spoil a ballot already cast def test_ballot_box_spoil_ballot(self) -> None: # Arrange encrypted_ballot = get_optional( encrypt_ballot( self.ballot, self.internal_manifest, self.context, self.seed, ) ) store: DataStore = DataStore() # Act ballot_box = BallotBox(self.internal_manifest, self.context, store) submitted_ballot = ballot_box.spoil(encrypted_ballot) # Assert # Test returned ballot self.assertIsNotNone(submitted_ballot) self.assertEqual(submitted_ballot.state, BallotBoxState.SPOILED) # Test ballot in box ballot_in_box = store.get(encrypted_ballot.object_id) self.assertIsNotNone(ballot_in_box) self.assertEqual(ballot_in_box.state, BallotBoxState.SPOILED) self.assertEqual(ballot_in_box.object_id, submitted_ballot.object_id) # Test failure modes self.assertIsNone(ballot_box.cast(encrypted_ballot)) # cannot cast again self.assertIsNone( ballot_box.spoil(encrypted_ballot) ) # cannot spoil a ballot already cast def test_submit_ballot_to_box(self) -> None: # Arrange encrypted_ballot = get_optional( encrypt_ballot( self.ballot, self.internal_manifest, self.context, self.seed, ) ) store: DataStore = DataStore() # Act submitted_ballot = submit_ballot_to_box( encrypted_ballot, BallotBoxState.CAST, self.internal_manifest, self.context, store, ) # Assert # Test returned ballot self.assertIsNotNone(submitted_ballot) self.assertEqual(submitted_ballot.state, BallotBoxState.CAST) # Test ballot in box ballot_in_box = store.get(encrypted_ballot.object_id) self.assertIsNotNone(ballot_in_box) self.assertEqual(ballot_in_box.state, BallotBoxState.CAST) self.assertEqual(ballot_in_box.object_id, submitted_ballot.object_id) # Test failure modes self.assertIsNone( submit_ballot_to_box( encrypted_ballot, BallotBoxState.CAST, self.internal_manifest, self.context, store, ) ) # cannot cast again self.assertIsNone( submit_ballot_to_box( encrypted_ballot, BallotBoxState.SPOILED, self.internal_manifest, self.context, store, ) ) # cannot spoil a ballot already cast def test_cast_ballot(self) -> None: # Arrange encrypted_ballot = get_optional( encrypt_ballot( self.ballot, self.internal_manifest, self.context, self.seed, ) ) # Act submitted_ballot = cast_ballot(encrypted_ballot) # Assert self.assertIsNotNone(submitted_ballot) self.assertEqual(submitted_ballot.state, BallotBoxState.CAST) self.assertEqual(encrypted_ballot.object_id, submitted_ballot.object_id) def test_spoil_ballot(self) -> None: # Arrange encrypted_ballot = get_optional( encrypt_ballot( self.ballot, self.internal_manifest, self.context, self.seed, ) ) # Act submitted_ballot = spoil_ballot(encrypted_ballot) # Assert self.assertIsNotNone(submitted_ballot) self.assertEqual(submitted_ballot.state, BallotBoxState.SPOILED) self.assertEqual(encrypted_ballot.object_id, submitted_ballot.object_id) def test_submit_ballot(self) -> None: # Arrange encrypted_ballot = get_optional( encrypt_ballot( self.ballot, self.internal_manifest, self.context, self.seed, ) ) # Act submitted_ballot = submit_ballot(encrypted_ballot, BallotBoxState.CAST) # Assert self.assertIsNotNone(submitted_ballot) self.assertEqual(submitted_ballot.state, BallotBoxState.CAST) self.assertEqual(encrypted_ballot.object_id, submitted_ballot.object_id) ================================================ FILE: tests/unit/electionguard/test_ballot_code.py ================================================ from tests.base_test_case import BaseTestCase from electionguard.group import ZERO_MOD_Q, ONE_MOD_Q, TWO_MOD_Q from electionguard.ballot_code import ( get_ballot_code, get_hash_for_device, ) from electionguard_tools.factories.election_factory import ElectionFactory class TestBallotCode(BaseTestCase): """Ballot code tests""" def test_rotate_ballot_code(self): # Arrange device = ElectionFactory.get_encryption_device() ballot_hash_1 = ONE_MOD_Q ballot_hash_2 = TWO_MOD_Q timestamp_1 = 1000 timestamp_2 = 2000 # Act device_hash = get_hash_for_device( device.device_id, device.session_id, device.launch_code, device.location ) ballot_code_1 = get_ballot_code(device_hash, timestamp_1, ballot_hash_1) ballot_code_2 = get_ballot_code(device_hash, timestamp_2, ballot_hash_2) # Assert self.assertIsNotNone(device_hash) self.assertIsNotNone(ballot_code_1) self.assertIsNotNone(ballot_code_2) self.assertNotEqual(device_hash, ZERO_MOD_Q) self.assertNotEqual(ballot_code_1, device_hash) self.assertNotEqual(ballot_code_2, device_hash) self.assertNotEqual(ballot_code_1, ballot_code_2) ================================================ FILE: tests/unit/electionguard/test_ballot_compact.py ================================================ from tests.base_test_case import BaseTestCase from electionguard.ballot import ( PlaintextBallot, SubmittedBallot, ) from electionguard.ballot_box import cast_ballot from electionguard.ballot_compact import ( compress_plaintext_ballot, compress_submitted_ballot, expand_compact_plaintext_ballot, expand_compact_submitted_ballot, ) from electionguard.election import CiphertextElectionContext from electionguard.elgamal import elgamal_keypair_from_secret from electionguard.encrypt import encrypt_ballot from electionguard.group import TWO_MOD_Q, ElementModQ from electionguard.manifest import InternalManifest from electionguard_tools.factories.election_factory import ElectionFactory class TestCompactBallot(BaseTestCase): """Test Compact Ballot Variations""" plaintext_ballot: PlaintextBallot ballot_nonce: ElementModQ submitted_ballot: SubmittedBallot internal_manifest: InternalManifest context: CiphertextElectionContext def setUp(self) -> None: # Election setup election_factory = ElectionFactory() keypair = elgamal_keypair_from_secret(TWO_MOD_Q) manifest = election_factory.get_fake_manifest() ( self.internal_manifest, self.context, ) = election_factory.get_fake_ciphertext_election(manifest, keypair.public_key) device_hash = ElectionFactory.get_encryption_device().get_hash() # Arrange ballots self.plaintext_ballot = election_factory.get_fake_ballot(self.internal_manifest) ciphertext_ballot = encrypt_ballot( self.plaintext_ballot, self.internal_manifest, self.context, device_hash ) self.ballot_nonce = ciphertext_ballot.nonce self.submitted_ballot = cast_ballot(ciphertext_ballot) def test_compact_plaintext_ballot(self) -> None: # Act compact_ballot = compress_plaintext_ballot(self.plaintext_ballot) # Assert self.assertIsNotNone(compact_ballot) self.assertEqual(self.plaintext_ballot.object_id, compact_ballot.object_id) # Act expanded_ballot = expand_compact_plaintext_ballot( compact_ballot, self.internal_manifest ) # Assert self.assertIsNotNone(expanded_ballot) self.assertEqual(self.plaintext_ballot, expanded_ballot) def test_compact_submitted_ballot(self) -> None: # Act compact_ballot = compress_submitted_ballot( self.submitted_ballot, self.plaintext_ballot, self.ballot_nonce ) # Assert self.assertIsNotNone(compact_ballot) self.assertEqual( self.submitted_ballot.object_id, compact_ballot.compact_plaintext_ballot.object_id, ) # Act expanded_ballot = expand_compact_submitted_ballot( compact_ballot, self.internal_manifest, self.context ) # Assert self.assertIsNotNone(expanded_ballot) self.assertEqual(self.submitted_ballot, expanded_ballot) self.assertEqual(self.submitted_ballot.crypto_hash, expanded_ballot.crypto_hash) ================================================ FILE: tests/unit/electionguard/test_constants.py ================================================ import os from unittest.mock import patch from tests.base_test_case import BaseTestCase from electionguard.constants import ( PrimeOption, LARGE_TEST_CONSTANTS, get_constants, STANDARD_CONSTANTS, ) from electionguard.constants import ( get_small_prime, get_large_prime, get_cofactor, get_generator, ) class TestConstants(BaseTestCase): """Election constant tests.""" @patch.dict(os.environ, {"PRIME_OPTION": PrimeOption.Standard.value}) def test_get_standard_primes(self): """Test getting standard constants with large primes.""" # Act constants = get_constants() # Assert self.assertIsNotNone(constants) self.assertEqual(constants, STANDARD_CONSTANTS) self.assertEqual(constants.large_prime, get_large_prime()) self.assertEqual(constants.small_prime, get_small_prime()) self.assertEqual(constants.cofactor, get_cofactor()) self.assertEqual(constants.generator, get_generator()) @patch.dict(os.environ, {"PRIME_OPTION": PrimeOption.TestOnly.value}) def test_get_test_primes(self): """Test getting test only constants with small primes.""" # Act constants = get_constants() # Assert self.assertIsNotNone(constants) self.assertEqual(constants, LARGE_TEST_CONSTANTS) self.assertEqual(constants.large_prime, get_large_prime()) self.assertEqual(constants.small_prime, get_small_prime()) self.assertEqual(constants.cofactor, get_cofactor()) self.assertEqual(constants.generator, get_generator()) ================================================ FILE: tests/unit/electionguard/test_decrypt_with_shares.py ================================================ # pylint: disable=protected-access # pylint: disable=too-many-instance-attributes # pylint: disable=unnecessary-comprehension from typing import Dict, List, Tuple from tests.base_test_case import BaseTestCase from electionguard.ballot_box import BallotBox, BallotBoxState, get_ballots from electionguard.data_store import DataStore from electionguard.decrypt_with_shares import ( decrypt_selection_with_decryption_shares, decrypt_ballot, ) from electionguard.decryption import ( compute_decryption_share, compute_decryption_share_for_ballot, compute_compensated_decryption_share_for_ballot, compute_lagrange_coefficients_for_guardians, reconstruct_decryption_share_for_ballot, ) from electionguard.decryption_share import DecryptionShare from electionguard.encrypt import EncryptionMediator from electionguard.group import ElementModP from electionguard.guardian import Guardian from electionguard.key_ceremony import CeremonyDetails from electionguard.key_ceremony_mediator import KeyCeremonyMediator from electionguard.tally import tally_ballots from electionguard.type import GuardianId from electionguard.utils import get_optional import electionguard_tools.factories.ballot_factory as BallotFactory import electionguard_tools.factories.election_factory as ElectionFactory from electionguard_tools.helpers.key_ceremony_orchestrator import ( KeyCeremonyOrchestrator, ) from electionguard_tools.helpers.tally_accumulate import accumulate_plaintext_ballots from electionguard_tools.helpers.election_builder import ElectionBuilder election_factory = ElectionFactory.ElectionFactory() ballot_factory = BallotFactory.BallotFactory() class TestDecryptWithShares(BaseTestCase): """Test decrypt with shares methods""" NUMBER_OF_GUARDIANS = 3 QUORUM = 2 CEREMONY_DETAILS = CeremonyDetails(NUMBER_OF_GUARDIANS, QUORUM) def setUp(self): # Key Ceremony self.key_ceremony_mediator = KeyCeremonyMediator( "key_ceremony_mediator_mediator", self.CEREMONY_DETAILS ) self.guardians: List[Guardian] = KeyCeremonyOrchestrator.create_guardians( self.CEREMONY_DETAILS ) KeyCeremonyOrchestrator.perform_full_ceremony( self.guardians, self.key_ceremony_mediator ) self.joint_public_key = self.key_ceremony_mediator.publish_joint_key() # Setup the election self.manifest = election_factory.get_fake_manifest() builder = ElectionBuilder(self.NUMBER_OF_GUARDIANS, self.QUORUM, self.manifest) builder.set_public_key(self.joint_public_key.joint_public_key) builder.set_commitment_hash(self.joint_public_key.commitment_hash) self.internal_manifest, self.context = get_optional(builder.build()) self.encryption_device = election_factory.get_encryption_device() self.ballot_marking_device = EncryptionMediator( self.internal_manifest, self.context, self.encryption_device ) # get some fake ballots self.fake_cast_ballot = ballot_factory.get_fake_ballot( self.internal_manifest, "some-unique-ballot-id-cast" ) self.more_fake_ballots = [] for i in range(10): self.more_fake_ballots.append( ballot_factory.get_fake_ballot( self.internal_manifest, f"some-unique-ballot-id-cast{i}" ) ) self.fake_spoiled_ballot = ballot_factory.get_fake_ballot( self.internal_manifest, "some-unique-ballot-id-spoiled" ) self.more_fake_spoiled_ballots = [] for i in range(2): self.more_fake_spoiled_ballots.append( ballot_factory.get_fake_ballot( self.internal_manifest, f"some-unique-ballot-id-spoiled{i}" ) ) self.assertTrue( self.fake_cast_ballot.is_valid( self.internal_manifest.ballot_styles[0].object_id ) ) self.assertTrue( self.fake_spoiled_ballot.is_valid( self.internal_manifest.ballot_styles[0].object_id ) ) self.expected_plaintext_tally = accumulate_plaintext_ballots( [self.fake_cast_ballot] + self.more_fake_ballots ) # Fill in the expected values with any missing selections # that were not made on any ballots selection_ids = { selection.object_id for contest in self.internal_manifest.contests for selection in contest.ballot_selections } missing_selection_ids = selection_ids.difference( set(self.expected_plaintext_tally) ) for id in missing_selection_ids: self.expected_plaintext_tally[id] = 0 # Encrypt self.encrypted_fake_cast_ballot = self.ballot_marking_device.encrypt( self.fake_cast_ballot ) self.encrypted_fake_spoiled_ballot = self.ballot_marking_device.encrypt( self.fake_spoiled_ballot ) # encrypt some more fake ballots self.more_fake_encrypted_ballots = [] for fake_ballot in self.more_fake_ballots: self.more_fake_encrypted_ballots.append( self.ballot_marking_device.encrypt(fake_ballot) ) # encrypt some more fake ballots self.more_fake_encrypted_spoiled_ballots = [] for fake_ballot in self.more_fake_spoiled_ballots: self.more_fake_encrypted_spoiled_ballots.append( self.ballot_marking_device.encrypt(fake_ballot) ) # configure the ballot box ballot_store = DataStore() ballot_box = BallotBox(self.internal_manifest, self.context, ballot_store) ballot_box.cast(self.encrypted_fake_cast_ballot) ballot_box.spoil(self.encrypted_fake_spoiled_ballot) # Cast some more fake ballots for fake_ballot in self.more_fake_encrypted_ballots: ballot_box.cast(fake_ballot) # Spoil some more fake ballots for fake_ballot in self.more_fake_encrypted_spoiled_ballots: ballot_box.spoil(fake_ballot) # generate encrypted tally self.ciphertext_tally = tally_ballots( ballot_store, self.internal_manifest, self.context ) self.ciphertext_ballots = get_ballots(ballot_store, BallotBoxState.SPOILED) def tearDown(self): self.key_ceremony_mediator.reset( CeremonyDetails(self.NUMBER_OF_GUARDIANS, self.QUORUM) ) def test_decrypt_selection_with_all_guardians_present(self): # Arrange available_guardians = self.guardians # find the first selection first_contest = list(self.ciphertext_tally.contests.values())[0] first_selection = list(first_contest.selections.values())[0] print(first_contest.object_id) print(first_selection.object_id) # precompute decryption shares for specific selection for the guardians shares: Dict[GuardianId, Tuple[ElementModP, DecryptionShare]] = { guardian.id: ( guardian.share_key().key, compute_decryption_share( guardian._election_keys, self.ciphertext_tally, self.context, ) .contests[first_contest.object_id] .selections[first_selection.object_id], ) for guardian in available_guardians } # Act result = decrypt_selection_with_decryption_shares( first_selection, shares, self.context.crypto_extended_base_hash ) # Assert self.assertIsNotNone(result) self.assertEqual( self.expected_plaintext_tally[first_selection.object_id], result.tally ) def test_decrypt_ballot_with_all_guardians_present(self): # Arrange # precompute decryption shares for the guardians available_guardians = self.guardians plaintext_ballot = self.fake_cast_ballot encrypted_ballot = self.encrypted_fake_cast_ballot shares = { available_guardian.id: compute_decryption_share_for_ballot( available_guardian._election_keys, encrypted_ballot, self.context, ) for available_guardian in available_guardians } # act result = decrypt_ballot( encrypted_ballot, shares, self.context.crypto_extended_base_hash, self.manifest, ) # assert self.assertIsNotNone(result) for contest in plaintext_ballot.contests: for selection in contest.ballot_selections: expected_tally = selection.vote actual_tally = ( result.contests[contest.object_id] .selections[selection.object_id] .tally ) self.assertEqual(expected_tally, actual_tally) def test_decrypt_ballot_with_missing_guardians(self): # Arrange # precompute decryption shares for the guardians plaintext_ballot = self.fake_cast_ballot encrypted_ballot = self.encrypted_fake_cast_ballot available_guardians = self.guardians[0:2] missing_guardian = self.guardians[2] available_shares = { available_guardian.id: compute_decryption_share_for_ballot( available_guardian._election_keys, encrypted_ballot, self.context, ) for available_guardian in available_guardians } compensated_shares = { available_guardian.id: compute_compensated_decryption_share_for_ballot( available_guardian.decrypt_backup( available_guardian._guardian_election_partial_key_backups.get( missing_guardian.id ) ), missing_guardian.share_key(), available_guardian.share_key(), encrypted_ballot, self.context, ) for available_guardian in available_guardians } lagrange_coefficients = compute_lagrange_coefficients_for_guardians( [guardian.share_key() for guardian in available_guardians] ) reconstructed_share = reconstruct_decryption_share_for_ballot( missing_guardian.share_key(), encrypted_ballot, compensated_shares, lagrange_coefficients, ) all_shares = {**available_shares, missing_guardian.id: reconstructed_share} # act result = decrypt_ballot( encrypted_ballot, all_shares, self.context.crypto_extended_base_hash, self.manifest, ) # assert self.assertIsNotNone(result) for contest in plaintext_ballot.contests: for selection in contest.ballot_selections: expected_tally = selection.vote actual_tally = ( result.contests[contest.object_id] .selections[selection.object_id] .tally ) self.assertEqual(expected_tally, actual_tally) ================================================ FILE: tests/unit/electionguard/test_decryption.py ================================================ # pylint: disable=protected-access # pylint: disable=too-many-instance-attributes # pylint: disable=unnecessary-comprehension from typing import Dict, List from tests.base_test_case import BaseTestCase from electionguard.ballot import SubmittedBallot from electionguard.ballot_box import BallotBox, BallotBoxState, get_ballots from electionguard.data_store import DataStore from electionguard.decrypt_with_shares import decrypt_selection_with_decryption_shares from electionguard.decryption import ( compute_compensated_decryption_share, compute_compensated_decryption_share_for_ballot, compute_decryption_share, compute_decryption_share_for_selection, compute_compensated_decryption_share_for_selection, compute_lagrange_coefficients_for_guardians, compute_recovery_public_key, reconstruct_decryption_share, reconstruct_decryption_share_for_ballot, ) from electionguard.decryption_share import ( CompensatedDecryptionShare, create_ciphertext_decryption_selection, ) from electionguard.election_polynomial import compute_lagrange_coefficient from electionguard.elgamal import ElGamalKeyPair from electionguard.group import ( ZERO_MOD_Q, mult_p, pow_p, ) from electionguard.encrypt import EncryptionMediator from electionguard.guardian import Guardian from electionguard.key_ceremony import ( CeremonyDetails, ElectionKeyPair, ) from electionguard.key_ceremony_mediator import KeyCeremonyMediator from electionguard.tally import tally_ballots from electionguard.type import BallotId, GuardianId from electionguard.utils import get_optional import electionguard_tools.factories.ballot_factory as BallotFactory import electionguard_tools.factories.election_factory as ElectionFactory from electionguard_tools.helpers.key_ceremony_orchestrator import ( KeyCeremonyOrchestrator, ) from electionguard_tools.helpers.tally_accumulate import accumulate_plaintext_ballots from electionguard_tools.helpers.election_builder import ElectionBuilder election_factory = ElectionFactory.ElectionFactory() ballot_factory = BallotFactory.BallotFactory() class TestDecryption(BaseTestCase): """Test decryption methods""" NUMBER_OF_GUARDIANS = 3 QUORUM = 2 CEREMONY_DETAILS = CeremonyDetails(NUMBER_OF_GUARDIANS, QUORUM) def setUp(self): # Key Ceremony self.key_ceremony_mediator = KeyCeremonyMediator( "key_ceremony_mediator_mediator", self.CEREMONY_DETAILS ) self.guardians: List[Guardian] = KeyCeremonyOrchestrator.create_guardians( self.CEREMONY_DETAILS ) KeyCeremonyOrchestrator.perform_full_ceremony( self.guardians, self.key_ceremony_mediator ) self.joint_public_key = self.key_ceremony_mediator.publish_joint_key() # Setup the election manifest = election_factory.get_fake_manifest() builder = ElectionBuilder(self.NUMBER_OF_GUARDIANS, self.QUORUM, manifest) builder.set_public_key(self.joint_public_key.joint_public_key) builder.set_commitment_hash(self.joint_public_key.commitment_hash) self.internal_manifest, self.context = get_optional(builder.build()) self.encryption_device = election_factory.get_encryption_device() self.ballot_marking_device = EncryptionMediator( self.internal_manifest, self.context, self.encryption_device ) # get some fake ballots self.fake_cast_ballot = ballot_factory.get_fake_ballot( self.internal_manifest, "some-unique-ballot-id-cast" ) self.more_fake_ballots = [] for i in range(10): self.more_fake_ballots.append( ballot_factory.get_fake_ballot( self.internal_manifest, f"some-unique-ballot-id-cast{i}" ) ) self.fake_spoiled_ballot = ballot_factory.get_fake_ballot( self.internal_manifest, "some-unique-ballot-id-spoiled" ) self.more_fake_spoiled_ballots = [] for i in range(2): self.more_fake_spoiled_ballots.append( ballot_factory.get_fake_ballot( self.internal_manifest, f"some-unique-ballot-id-spoiled{i}" ) ) self.assertTrue( self.fake_cast_ballot.is_valid( self.internal_manifest.ballot_styles[0].object_id ) ) self.assertTrue( self.fake_spoiled_ballot.is_valid( self.internal_manifest.ballot_styles[0].object_id ) ) self.expected_plaintext_tally = accumulate_plaintext_ballots( [self.fake_cast_ballot] + self.more_fake_ballots ) # Fill in the expected values with any missing selections # that were not made on any ballots selection_ids = { selection.object_id for contest in self.internal_manifest.contests for selection in contest.ballot_selections } missing_selection_ids = selection_ids.difference( set(self.expected_plaintext_tally) ) for id in missing_selection_ids: self.expected_plaintext_tally[id] = 0 # Encrypt self.encrypted_fake_cast_ballot = self.ballot_marking_device.encrypt( self.fake_cast_ballot ) self.encrypted_fake_spoiled_ballot = self.ballot_marking_device.encrypt( self.fake_spoiled_ballot ) # encrypt some more fake ballots self.more_fake_encrypted_ballots = [] for fake_ballot in self.more_fake_ballots: self.more_fake_encrypted_ballots.append( self.ballot_marking_device.encrypt(fake_ballot) ) # encrypt some more fake ballots self.more_fake_encrypted_spoiled_ballots = [] for fake_ballot in self.more_fake_spoiled_ballots: self.more_fake_encrypted_spoiled_ballots.append( self.ballot_marking_device.encrypt(fake_ballot) ) # configure the ballot box ballot_store = DataStore() ballot_box = BallotBox(self.internal_manifest, self.context, ballot_store) ballot_box.cast(self.encrypted_fake_cast_ballot) ballot_box.spoil(self.encrypted_fake_spoiled_ballot) # Cast some more fake ballots for fake_ballot in self.more_fake_encrypted_ballots: ballot_box.cast(fake_ballot) # Spoil some more fake ballots for fake_ballot in self.more_fake_encrypted_spoiled_ballots: ballot_box.spoil(fake_ballot) # generate encrypted tally self.ciphertext_tally = tally_ballots( ballot_store, self.internal_manifest, self.context ) self.ciphertext_ballots: Dict[BallotId, SubmittedBallot] = get_ballots( ballot_store, BallotBoxState.SPOILED ) def tearDown(self): self.key_ceremony_mediator.reset( CeremonyDetails(self.NUMBER_OF_GUARDIANS, self.QUORUM) ) # SHARE def test_compute_decryption_share(self): # Arrange guardian = self.guardians[0] # Act # Guardian doesn't give keys broken_secret_key = ZERO_MOD_Q broken_guardian_key_pair = ElectionKeyPair( guardian.id, guardian.sequence_order, ElGamalKeyPair( broken_secret_key, guardian._election_keys.key_pair.public_key ), guardian._election_keys.polynomial, ) broken_share = compute_decryption_share( broken_guardian_key_pair, self.ciphertext_tally, self.context, ) # Assert self.assertIsNone(broken_share) # Act # Normal use case share = compute_decryption_share( guardian._election_keys, self.ciphertext_tally, self.context, ) # Assert self.assertIsNotNone(share) def test_compute_compensated_decryption_share(self): # Arrange guardian = self.guardians[0] missing_guardian = self.guardians[2] missing_guardian_public_key = missing_guardian.share_key() missing_guardian_backup = missing_guardian._backups_to_share.get(guardian.id) # Act missing_guardian_coordinate = guardian.decrypt_backup(missing_guardian_backup) share = compute_compensated_decryption_share( missing_guardian_coordinate, guardian.share_key(), missing_guardian_public_key, self.ciphertext_tally, self.context, ) # Assert self.assertIsNotNone(share) # SELECTION def test_compute_selection(self): # Arrange first_selection = [ selection for contest in self.ciphertext_tally.contests.values() for selection in contest.selections.values() ][0] # act result = compute_decryption_share_for_selection( self.guardians[0]._election_keys, first_selection, self.context ) # assert self.assertIsNotNone(result) def test_compute_compensated_selection(self): """ demonstrates the complete workflow for computing a compensated decryption share For one selection. It is useful for verifying that the workflow is correct """ # Arrange available_guardian_1 = self.guardians[0] available_guardian_2 = self.guardians[1] missing_guardian = self.guardians[2] available_guardian_1_key = available_guardian_1.share_key() available_guardian_2_key = available_guardian_2.share_key() missing_guardian_key = missing_guardian.share_key() first_selection = [ selection for contest in self.ciphertext_tally.contests.values() for selection in contest.selections.values() ][0] # Compute lagrange coefficients for the guardians that are present lagrange_0 = compute_lagrange_coefficient( available_guardian_1.sequence_order, *[available_guardian_2.sequence_order], ) lagrange_1 = compute_lagrange_coefficient( available_guardian_2.sequence_order, *[available_guardian_1.sequence_order], ) print( ( f"lagrange: sequence_orders: ({available_guardian_1.sequence_order}, " f"{available_guardian_2.sequence_order}, {missing_guardian.sequence_order})\n" ) ) print(lagrange_0) print(lagrange_1) # compute their shares share_0 = compute_decryption_share_for_selection( available_guardian_1._election_keys, first_selection, self.context ) share_1 = compute_decryption_share_for_selection( available_guardian_2._election_keys, first_selection, self.context ) self.assertIsNotNone(share_0) self.assertIsNotNone(share_1) # compute compensations shares for the missing guardian g3s_encrypted_backup_for_g1 = ( missing_guardian.share_election_partial_key_backup(available_guardian_1.id) ) g1s_copy_of_g3s_coordinate = available_guardian_1.decrypt_backup( g3s_encrypted_backup_for_g1 ) compensation_0 = compute_compensated_decryption_share_for_selection( g1s_copy_of_g3s_coordinate, available_guardian_1.share_key(), missing_guardian.share_key(), first_selection, self.context, ) g3s_encrypted_backup_for_g2 = ( missing_guardian.share_election_partial_key_backup(available_guardian_2.id) ) g2s_copy_of_g3s_coordinate = available_guardian_2.decrypt_backup( g3s_encrypted_backup_for_g2 ) compensation_1 = compute_compensated_decryption_share_for_selection( g2s_copy_of_g3s_coordinate, available_guardian_2.share_key(), missing_guardian.share_key(), first_selection, self.context, ) self.assertIsNotNone(compensation_0) self.assertIsNotNone(compensation_1) print("\nSHARES:") print(compensation_0) print(compensation_1) # Check the share proofs self.assertTrue( compensation_0.proof.is_valid( first_selection.ciphertext, compute_recovery_public_key( available_guardian_1_key, missing_guardian_key ), compensation_0.share, self.context.crypto_extended_base_hash, ) ) self.assertTrue( compensation_1.proof.is_valid( first_selection.ciphertext, compute_recovery_public_key( available_guardian_2_key, missing_guardian_key ), compensation_1.share, self.context.crypto_extended_base_hash, ) ) share_pow_p = [ pow_p(compensation_0.share, lagrange_0), pow_p(compensation_1.share, lagrange_1), ] print("\nSHARE_POW_P") print(share_pow_p) # reconstruct the missing share from the compensation shares reconstructed_share = mult_p( *[ pow_p(compensation_0.share, lagrange_0), pow_p(compensation_1.share, lagrange_1), ] ) print("\nRECONSTRUCTED SHARE\n") print(reconstructed_share) share_2 = create_ciphertext_decryption_selection( first_selection.object_id, missing_guardian.id, reconstructed_share, { available_guardian_1.id: compensation_0, available_guardian_2.id: compensation_1, }, ) # Decrypt the result result = decrypt_selection_with_decryption_shares( first_selection, { available_guardian_1.id: ( available_guardian_1.share_key().key, share_0, ), available_guardian_2.id: ( available_guardian_2.share_key().key, share_1, ), missing_guardian.id: ( missing_guardian.share_key().key, share_2, ), }, self.context.crypto_extended_base_hash, ) print(result) self.assertIsNotNone(result) self.assertEqual( result.tally, self.expected_plaintext_tally[first_selection.object_id] ) def test_compute_compensated_selection_failure(self): # Arrange available_guardian = self.guardians[0] missing_guardian = self.guardians[2] first_selection = [ selection for contest in self.ciphertext_tally.contests.values() for selection in contest.selections.values() ][0] # Act # Get backup for missing guardian instead of one sent by guardian incorrect_backup_encrypted = ( available_guardian.share_election_partial_key_backup(missing_guardian.id) ) incorrect_backup_decrypted = missing_guardian.decrypt_backup( incorrect_backup_encrypted ) result = compute_compensated_decryption_share_for_selection( incorrect_backup_decrypted, available_guardian.share_key(), missing_guardian.share_key(), first_selection, self.context, ) # Assert self.assertIsNone(result) def test_reconstruct_decryption_share(self): # Arrange available_guardians = self.guardians[0:2] available_guardians_keys = [ guardian.share_key() for guardian in available_guardians ] missing_guardian = self.guardians[2] missing_guardian_key = missing_guardian.share_key() missing_guardian_backups = { backup.designated_id: backup for backup in missing_guardian.share_election_partial_key_backups() } tally = self.ciphertext_tally # Act compensated_shares: Dict[GuardianId, CompensatedDecryptionShare] = { available_guardian.id: compute_compensated_decryption_share( available_guardian.decrypt_backup( missing_guardian_backups[available_guardian.id] ), available_guardian.share_key(), missing_guardian_key, tally, self.context, ) for available_guardian in available_guardians } lagrange_coefficients = compute_lagrange_coefficients_for_guardians( available_guardians_keys ) share = reconstruct_decryption_share( missing_guardian_key, tally, compensated_shares, lagrange_coefficients ) # Assert self.assertEqual(self.QUORUM, len(compensated_shares)) self.assertEqual(self.QUORUM, len(lagrange_coefficients)) self.assertIsNotNone(share) def test_reconstruct_decryption_shares_for_ballot(self): # Arrange available_guardians = self.guardians[0:2] available_guardians_keys = [ guardian.share_key() for guardian in available_guardians ] missing_guardian = self.guardians[2] missing_guardian_key = missing_guardian.share_key() missing_guardian_backups = { backup.designated_id: backup for backup in missing_guardian.share_election_partial_key_backups() } ballot = list(self.ciphertext_ballots.values())[0] # Act compensated_ballot_shares: Dict[GuardianId, CompensatedDecryptionShare] = {} for available_guardian in available_guardians: backup = available_guardian.decrypt_backup( missing_guardian_backups[available_guardian.id] ) compensated_share = compute_compensated_decryption_share_for_ballot( backup, missing_guardian_key, available_guardian.share_key(), ballot, self.context, ) if compensated_share: compensated_ballot_shares[available_guardian.id] = compensated_share lagrange_coefficients = compute_lagrange_coefficients_for_guardians( available_guardians_keys ) missing_ballot_share = reconstruct_decryption_share_for_ballot( missing_guardian_key, ballot, compensated_ballot_shares, lagrange_coefficients, ) # Assert self.assertEqual(self.QUORUM, len(lagrange_coefficients)) self.assertEqual(len(available_guardians), len(compensated_ballot_shares)) self.assertEqual(len(available_guardians), len(lagrange_coefficients)) self.assertIsNotNone(missing_ballot_share) def test_reconstruct_decryption_share_for_ballot(self): # Arrange available_guardians = self.guardians[0:2] available_guardians_keys = [ guardian.share_key() for guardian in available_guardians ] missing_guardian = self.guardians[2] missing_guardian_key = missing_guardian.share_key() missing_guardian_backups = { backup.designated_id: backup for backup in missing_guardian.share_election_partial_key_backups() } ballot = self.ciphertext_ballots[self.fake_spoiled_ballot.object_id] # Act compensated_shares: Dict[GuardianId, CompensatedDecryptionShare] = { available_guardian.id: get_optional( compute_compensated_decryption_share_for_ballot( available_guardian.decrypt_backup( missing_guardian_backups[available_guardian.id] ), missing_guardian_key, available_guardian.share_key(), ballot, self.context, ) ) for available_guardian in available_guardians } lagrange_coefficients = compute_lagrange_coefficients_for_guardians( available_guardians_keys ) share = reconstruct_decryption_share_for_ballot( missing_guardian_key, ballot, compensated_shares, lagrange_coefficients ) # Assert self.assertEqual(self.QUORUM, len(compensated_shares)) self.assertEqual(self.QUORUM, len(lagrange_coefficients)) self.assertIsNotNone(share) ================================================ FILE: tests/unit/electionguard/test_election_polynomial.py ================================================ from tests.base_test_case import BaseTestCase from electionguard.schnorr import make_schnorr_proof from electionguard.elgamal import ElGamalKeyPair from electionguard.group import rand_q from electionguard.election_polynomial import ( Coefficient, compute_polynomial_coordinate, ElectionPolynomial, generate_polynomial, verify_polynomial_coordinate, ) from electionguard.group import ONE_MOD_P, ONE_MOD_Q, TWO_MOD_P, TWO_MOD_Q TEST_EXPONENT_MODIFIER = 1 TEST_POLYNOMIAL_DEGREE = 3 class TestElectionPolynomial(BaseTestCase): """Election polynomial tests""" def test_generate_polynomial(self): # Act polynomial = generate_polynomial(TEST_POLYNOMIAL_DEGREE) # Assert self.assertIsNotNone(polynomial) def test_compute_polynomial_coordinate(self): # create proofs proof_one = make_schnorr_proof(ElGamalKeyPair(ONE_MOD_Q, ONE_MOD_P), rand_q()) proof_two = make_schnorr_proof(ElGamalKeyPair(TWO_MOD_Q, TWO_MOD_P), rand_q()) # Arrange polynomial = ElectionPolynomial( [ Coefficient(ONE_MOD_Q, ONE_MOD_P, proof_one), Coefficient(TWO_MOD_Q, TWO_MOD_P, proof_two), ] ) # Act value = compute_polynomial_coordinate(TEST_EXPONENT_MODIFIER, polynomial) # Assert self.assertIsNotNone(value) def test_verify_polynomial_coordinate(self): # Arrange polynomial = generate_polynomial(TEST_POLYNOMIAL_DEGREE) # Act value = compute_polynomial_coordinate(TEST_EXPONENT_MODIFIER, polynomial) # Assert self.assertTrue( verify_polynomial_coordinate( value, TEST_EXPONENT_MODIFIER, polynomial.get_commitments() ) ) ================================================ FILE: tests/unit/electionguard/test_elgamal.py ================================================ from electionguard import ElGamalKeyPair from electionguard.elgamal import ( ElGamalPublicKey, ElGamalSecretKey, hashed_elgamal_encrypt, ) from electionguard.group import ElementModQ from electionguard.byte_padding import add_padding, remove_padding from electionguard.utils import get_optional from tests.base_test_case import BaseTestCase class TestElgamal(BaseTestCase): """Test decryption methods""" def test_hashed_elgamal_with_session_key_that_starts_with_0(self) -> None: kp = ElGamalKeyPair( secret_key=ElGamalSecretKey("02"), public_key=ElGamalPublicKey("A147CA31DE0F48C1"), ) plaintext = b"\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00" # these values produce a session_key 2B00, which produces encrypted # data that begins with a 0 byte. When hashed_elgamal_encrypt() returns that # data it calls bytes_to_hex() which truncates leading zero's. That produces # 255 bytes instead of 256 bytes and decrypt() much pad a byte to account for it. hmac_nonce = ElementModQ("D8E1") hmac_seed = ElementModQ("02F1") self.do_hashed_elgamal(kp, plaintext, hmac_nonce, hmac_seed) def test_hashed_elgamal_with_session_key_that_starts_with_1(self) -> None: kp = ElGamalKeyPair( secret_key=ElGamalSecretKey("02"), public_key=ElGamalPublicKey("A147CA31DE0F48C1"), ) plaintext = b"\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00" # these values produce a session_key EE19 which produces encrypted data that # begins with a 1 byte and bytes_to_hex() does not truncate anything hmac_nonce = ElementModQ("AB29") hmac_seed = ElementModQ("2179") self.do_hashed_elgamal(kp, plaintext, hmac_nonce, hmac_seed) def do_hashed_elgamal( self, kp: ElGamalKeyPair, plaintext: bytes, hmac_nonce: ElementModQ, hmac_seed: ElementModQ, ) -> None: padded_plaintext = add_padding(plaintext) hmac = hashed_elgamal_encrypt( padded_plaintext, hmac_nonce, kp.public_key, hmac_seed ) decryption_bytes_padded = hmac.decrypt(kp.secret_key, hmac_seed) self.assertIsNotNone(decryption_bytes_padded) decryption_bytes = remove_padding(get_optional(decryption_bytes_padded)) self.assertEqual(plaintext, decryption_bytes) ================================================ FILE: tests/unit/electionguard/test_encrypt.py ================================================ import os from unittest.mock import patch from unittest import TestCase from electionguard import PrimeOption from electionguard.byte_padding import TruncationError from electionguard.elgamal import ( HashedElGamalCiphertext, elgamal_keypair_from_secret, ) from electionguard.encrypt import ( ContestData, ContestErrorType, contest_from, encrypt_contest, ) from electionguard.group import ONE_MOD_Q, TWO_MOD_Q, ElementModP, ElementModQ, rand_q from electionguard.manifest import ( SelectionDescription, VoteVariationType, ContestDescriptionWithPlaceholders, ) from electionguard.serialize import to_raw from electionguard.utils import get_optional def get_sample_contest_description() -> ContestDescriptionWithPlaceholders: ballot_selections = [ SelectionDescription( "some-object-id-affirmative", 0, "some-candidate-id-affirmative" ), SelectionDescription( "some-object-id-negative", 1, "some-candidate-id-negative" ), ] placeholder_selections = [ SelectionDescription( "some-object-id-placeholder", 2, "some-candidate-id-placeholder" ) ] metadata = ContestDescriptionWithPlaceholders( "some-contest-object-id", 0, "some-electoral-district-id", VoteVariationType.one_of_m, 1, 1, "some-referendum-contest-name", ballot_selections, None, None, placeholder_selections, ) return metadata class TestEncrypt(TestCase): """Test encryption""" def test_contest_data_conversion(self) -> None: """Test contest data encoding to padded to bytes then decoding.""" # Arrange error = ContestErrorType.OverVote error_data = ["overvote-id-1", "overvote-id-2", "overvote-id-3"] write_ins = { "writein-id-1": "Teri Dactyl", "writein-id-2": "Allie Grater", "writein-id-3": "Anna Littlical", "writein-id-4": "Polly Wannakrakouer", } overflow_error_data = ["overflow-id" * 50] empty_contest_data = ContestData() write_in_contest_data = ContestData(write_ins=write_ins) overvote_contest_data = ContestData(error, error_data) overvote_and_write_in_contest_data = ContestData(error, error_data, write_ins) overflow_contest_data = ContestData(error, overflow_error_data, write_ins) # Act & Assert self._padding_cycle(empty_contest_data) self._padding_cycle(write_in_contest_data) self._padding_cycle(overvote_contest_data) self._padding_cycle(overvote_and_write_in_contest_data) self._padding_cycle(overflow_contest_data) def _padding_cycle(self, data: ContestData) -> None: """Run full cycle of padding and unpadding.""" EXPECTED_PADDED_LENGTH = 512 try: padded = data.to_bytes() unpadded = ContestData.from_bytes(padded) self.assertEqual(EXPECTED_PADDED_LENGTH, len(padded)) self.assertEqual(data, unpadded) except TruncationError: # Validate JSON exceeds allowed length json = to_raw(data) self.assertLess(EXPECTED_PADDED_LENGTH, len(json)) def test_encrypt_simple_contest_referendum_succeeds(self) -> None: # Arrange keypair = get_optional(elgamal_keypair_from_secret(TWO_MOD_Q)) nonce = rand_q() encryption_seed = ONE_MOD_Q contest_description = get_sample_contest_description() contest = contest_from(contest_description) contest_hash = contest_description.crypto_hash() # Act encrypted_contest = encrypt_contest( contest, contest_description, keypair.public_key, encryption_seed, nonce, should_verify_proofs=True, ) # Assert self.assertIsNotNone(encrypted_contest) if encrypted_contest is not None: self.assertTrue( encrypted_contest.is_valid_encryption( contest_hash, keypair.public_key, encryption_seed ) ) def test_contest_encrypt_with_overvotes(self) -> None: # Arrange keypair = get_optional(elgamal_keypair_from_secret(TWO_MOD_Q)) nonce = rand_q() encryption_seed = ONE_MOD_Q contest_description = get_sample_contest_description() contest = contest_from(contest_description) contest_hash = contest_description.crypto_hash() # Add Overvotes for selection in contest.ballot_selections: selection.vote = 1 # Act encrypted_contest = encrypt_contest( contest, contest_description, keypair.public_key, encryption_seed, nonce, should_verify_proofs=True, ) # Assert self.assertIsNotNone(encrypted_contest) self.assertIsNotNone(encrypted_contest.extended_data) self.assertTrue( encrypted_contest.is_valid_encryption( contest_hash, keypair.public_key, encryption_seed ) ) # Act decrypted_data = get_optional( encrypted_contest.extended_data.decrypt(keypair.secret_key, encryption_seed) ) contest_data = ContestData.from_bytes(decrypted_data) # Assert self.assertIsNotNone(contest_data) self.assertIsNotNone(contest_data.error) self.assertIsNotNone(contest_data.error_data) self.assertEqual(contest_data.error, ContestErrorType.OverVote) self.assertGreater(len(contest_data.error_data), 0) def test_contest_encrypt_with_write_ins(self): # Arrange keypair = get_optional(elgamal_keypair_from_secret(TWO_MOD_Q)) nonce = rand_q() encryption_seed = ONE_MOD_Q contest_description = get_sample_contest_description() contest = contest_from(contest_description) contest_hash = contest_description.crypto_hash() write_in_value = "write_in" # Add Write-ins for selection in contest.ballot_selections: selection.write_in = write_in_value # Act encrypted_contest = encrypt_contest( contest, contest_description, keypair.public_key, encryption_seed, nonce, should_verify_proofs=True, ) # Assert self.assertIsNotNone(encrypted_contest) self.assertIsNotNone(encrypted_contest.extended_data) self.assertTrue( encrypted_contest.is_valid_encryption( contest_hash, keypair.public_key, encryption_seed ) ) # Act decrypted_data = get_optional( encrypted_contest.extended_data.decrypt(keypair.secret_key, encryption_seed) ) contest_data = ContestData.from_bytes(decrypted_data) # Assert self.assertIsNotNone(contest_data) self.assertIsNotNone(contest_data.write_ins) if contest_data is not None and contest_data.write_ins is not None: self.assertGreater(len(contest_data.write_ins), 0) for write_in in contest_data.write_ins.values(): self.assertEqual(write_in, write_in_value) @patch.dict(os.environ, {"PRIME_OPTION": PrimeOption.Standard.value}) def test_contest_data_integration(self) -> None: """Contest data encryption done with production primes to match other repositories.""" # Arrange keypair = get_optional( elgamal_keypair_from_secret( ElementModQ( "094CDA6CEB3332D62438B6D37BBA774D23C420FA019368671AD330AD50456603" ) ) ) encryption_seed = ElementModQ( "6E418518C6C244CA58399C0F47A9C761BAE7B876F8F5360D8D15FCFF26A42BAA" ) # pylint: disable=line-too-long encrypted_contest_data = HashedElGamalCiphertext( ElementModP( "C102BAB526517D74FE5D5C249E7F422993C0306C40A9398FBAD01A0D3547B50BDFD77C6EFC187C7B1FD7918A0B3C2A2FB0A3776A7240F9A75410569379B3D16877B547F52E79542C1129F6E369F2D006D0A1AA3919F0228CA07F5C9A4DFD1118A606AA4B7000F9EDC65963F130663FD4F7246F7CFE7A38F1E1DC9BC0698CAB881DCD5A75E6D7165B329C28D80B719D7A2ED50031A2448A4528275FF161F541CFE304A28CBE7193A4BF8676B2D4F2DE68F175C5B4BFD14B4B1D9868D00E0BD95B6491C96460159DEABF85239B10A7C86B3D975EF58BBF833C6ABFFF223DAF78C1AE4C6F64D084C4118F3B5A2618628FA18852BAB55DCE95C04FFCBBAF582D75C7B8B830424C74A8F8EACD154300FD67CF753EE14FCE94DDED95F1DD2C1386D92B3FF03A9D6EDEE0F67EC80C72E6425B4EA1C17D7B9CC5B2165905373A4E304496462CE2BA077F195302A39C52F0077CA682BC718074F928040D1A36F585AC187A741F51C843C5ED88BC5FB8B86ED96C42BCF84EDF833489D7D3AC407C6D0740CC94BA1D5B885EB430CE8C6017F8660A6C72F4378BF133AA663DBA36CAB967AAC0F7738478110ECEABAE3E914CB7A796C5394F7DF150940BEA43264765B34851ADE4E5F1F213C25DCF66D35BE92611555D8C05ACFDF1AC5CA82B7D7F0D9BE49596F8B7F3269D9887F40B4BAB5C3D2BA7049B6D2119C3D0D01501836203412869E0" ), "F8E994D157A065A1DB2DA5E38645C283F7CCB339E13F0DE29B83A4EFA2F4366C626FC8E318AF81DCB2E6083A598F8916A5FEEC3C1A1B8EBEB4081F3CB92FA86E000B4994B77EE173072D796D21EE771F4D8F50E7DC50A7945E35059F893DD0A67C53DF5A3439A89E990C5B7568912CD2655B39E943511E1B0DF8A8E1FEF4EAC3923A5B5DDF1A658335E97AA6EB12E4EEE1394D91548F3E8446E9BBF4207D873F54298B446A7D689FF60A6F60B3FC6B8319EC17FA424F0461949CD49B764C6360AC0D492696E43EE83A6A7CE7AEA4DDBA206F365AA81E918F63709DE796F0338CCD311360D97CDC821506D3EDB434922264966B8AF7E304A403E18384DDCF53AEF1FFC19A66FBCD9C2D04EFC8F2D456BE52DB9C460E3CA10AC4ABFE0B726E19A715546F1CD9CA89C57ED52DA9D78C30BEFE5FE99A8BDEA33B7C06EDFD4E92D514661CD55B99B54E5C468118E16F4827F78FB381845B093F202111E3B84CFCF8DAEE7948BA57698475F3EBC3729559835BF63AAB0F5659019965A2F0CF55E953B1CD37BCBED8EA0D5F161D461E03031BA7D0B042B978F7F6776DDFBCAA7145DE30BA24C29BDFA05C7CCF54D7DD58E75143A16F8619053FCF4DE7BDCCA031F0873A65ACCC56FE78F32B8FC192D2106CF1A1E5339A5C5657E6703D7F30F908CEEF05A84C67C426B187CBC1599FB334307146EAECB16774C5CB7630F4CB093E840086", "BBCDE57B7E92BB8607696E09FE629A2B9665D809649B751333023983C001C191", ) # Act decrypted_contest_data = encrypted_contest_data.decrypt( keypair.secret_key, encryption_seed ) contest_data = ContestData.from_bytes(decrypted_contest_data) self.assertIsNotNone(contest_data) self.assertEqual(contest_data.error, ContestErrorType.OverVote) self.assertEqual(len(contest_data.error_data), 3) self.assertEqual(len(contest_data.write_ins), 1) self.assertEqual( contest_data.write_ins["write-in-selection"], "Susan B. Anthony" ) ================================================ FILE: tests/unit/electionguard/test_guardian.py ================================================ # pylint: disable=too-many-public-methods from tests.base_test_case import BaseTestCase from electionguard.guardian import Guardian NUMBER_OF_GUARDIANS = 2 QUORUM = 2 SENDER_GUARDIAN_ID = "Test Guardian 1" SENDER_SEQUENCE_ORDER = 1 RECIPIENT_GUARDIAN_ID = "Test Guardian 2" RECIPIENT_SEQUENCE_ORDER = 2 ALTERNATE_VERIFIER_ID = "Test Verifier" ALTERNATE_VERIFIER_SEQUENCE_ORDER = 3 ELECTION_PUBLIC_KEY = "" class TestGuardian(BaseTestCase): """Guardian tests""" def test_import_from_guardian_private_record(self) -> None: # Arrange guardian_expected = Guardian.from_nonce( SENDER_GUARDIAN_ID, SENDER_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM ) private_guardian_record = guardian_expected.export_private_data() # Act guardian_actual = Guardian.from_private_record( private_guardian_record, NUMBER_OF_GUARDIANS, QUORUM ) # Assert # pylint: disable=protected-access self.assertEqual( guardian_actual._election_keys, guardian_expected._election_keys ) self.assertEqual( guardian_actual._guardian_election_public_keys, guardian_expected._guardian_election_public_keys, ) self.assertEqual( guardian_actual._guardian_election_partial_key_backups, guardian_expected._guardian_election_partial_key_backups, ) def test_set_ceremony_details(self) -> None: # Arrange guardian = Guardian.from_nonce( SENDER_GUARDIAN_ID, SENDER_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM ) expected_number_of_guardians = 10 expected_quorum = 4 # Act guardian.set_ceremony_details(expected_number_of_guardians, expected_quorum) # Assert self.assertEqual( expected_number_of_guardians, guardian.ceremony_details.number_of_guardians ) self.assertEqual(expected_quorum, guardian.ceremony_details.quorum) def test_share_key(self) -> None: # Arrange guardian = Guardian.from_nonce( SENDER_GUARDIAN_ID, SENDER_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM ) # Act key = guardian.share_key() # Assert self.assertIsNotNone(key) self.assertIsNotNone(key.key) self.assertEqual(key.owner_id, SENDER_GUARDIAN_ID) for proof in key.coefficient_proofs: self.assertTrue(proof.is_valid()) def test_save_guardian_key(self) -> None: # Arrange guardian = Guardian.from_nonce( SENDER_GUARDIAN_ID, SENDER_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM ) other_guardian = Guardian.from_nonce( RECIPIENT_GUARDIAN_ID, RECIPIENT_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM ) key = other_guardian.share_key() # Act guardian.save_guardian_key(key) # Assert self.assertTrue(guardian.all_guardian_keys_received()) def test_all_guardian_keys_received(self) -> None: # Arrange guardian = Guardian.from_nonce( SENDER_GUARDIAN_ID, SENDER_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM ) other_guardian = Guardian.from_nonce( RECIPIENT_GUARDIAN_ID, RECIPIENT_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM ) key = other_guardian.share_key() # Act self.assertFalse(guardian.all_guardian_keys_received()) guardian.save_guardian_key(key) # Assert self.assertTrue(guardian.all_guardian_keys_received()) def test_share_backups(self) -> None: # Arrange guardian = Guardian.from_nonce( SENDER_GUARDIAN_ID, SENDER_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM ) other_guardian = Guardian.from_nonce( RECIPIENT_GUARDIAN_ID, RECIPIENT_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM ) guardian.save_guardian_key(other_guardian.share_key()) # Act empty_key_backup = guardian.share_election_partial_key_backup(other_guardian.id) # Assert self.assertIsNone(empty_key_backup) # Act guardian.generate_election_partial_key_backups() key_backup = guardian.share_election_partial_key_backup(other_guardian.id) # Assert self.assertIsNotNone(key_backup) self.assertIsNotNone(key_backup.encrypted_coordinate) self.assertEqual(key_backup.owner_id, guardian.id) self.assertEqual(key_backup.designated_id, other_guardian.id) def test_save_election_partial_key_backup(self) -> None: # Arrange guardian = Guardian.from_nonce( SENDER_GUARDIAN_ID, SENDER_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM ) other_guardian = Guardian.from_nonce( RECIPIENT_GUARDIAN_ID, RECIPIENT_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM ) guardian.save_guardian_key(other_guardian.share_key()) guardian.generate_election_partial_key_backups() key_backup = guardian.share_election_partial_key_backup(other_guardian.id) # Act other_guardian.save_election_partial_key_backup(key_backup) # Assert self.assertTrue(other_guardian.all_election_partial_key_backups_received()) def test_all_election_partial_key_backups_received(self) -> None: # Arrange # Round 1 guardian = Guardian.from_nonce( SENDER_GUARDIAN_ID, SENDER_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM ) other_guardian = Guardian.from_nonce( RECIPIENT_GUARDIAN_ID, RECIPIENT_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM ) guardian.save_guardian_key(other_guardian.share_key()) other_guardian.save_guardian_key(guardian.share_key()) # Round 2 guardian.generate_election_partial_key_backups() key_backup = guardian.share_election_partial_key_backup(other_guardian.id) # Assert self.assertFalse(other_guardian.all_election_partial_key_backups_received()) other_guardian.save_election_partial_key_backup(key_backup) self.assertTrue(other_guardian.all_election_partial_key_backups_received()) def test_verify_election_partial_key_backup(self) -> None: # Arrange # Round 1 guardian = Guardian.from_nonce( SENDER_GUARDIAN_ID, SENDER_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM ) other_guardian = Guardian.from_nonce( RECIPIENT_GUARDIAN_ID, RECIPIENT_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM ) guardian.save_guardian_key(other_guardian.share_key()) other_guardian.save_guardian_key(guardian.share_key()) # Round 2 guardian.generate_election_partial_key_backups() key_backup = guardian.share_election_partial_key_backup(other_guardian.id) other_guardian.save_election_partial_key_backup(key_backup) # Act verification = other_guardian.verify_election_partial_key_backup( guardian.id, ) # Assert self.assertIsNotNone(verification) self.assertEqual(verification.owner_id, guardian.id) self.assertEqual(verification.designated_id, other_guardian.id) self.assertEqual(verification.verifier_id, other_guardian.id) self.assertTrue(verification.verified) def test_verify_election_partial_key_challenge(self) -> None: # Arrange guardian = Guardian.from_nonce( SENDER_GUARDIAN_ID, SENDER_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM ) other_guardian = Guardian.from_nonce( RECIPIENT_GUARDIAN_ID, RECIPIENT_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM ) alternate_verifier = Guardian.from_nonce( ALTERNATE_VERIFIER_ID, ALTERNATE_VERIFIER_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM, ) guardian.save_guardian_key(other_guardian.share_key()) guardian.generate_election_partial_key_backups() challenge = guardian.publish_election_backup_challenge(other_guardian.id) # Act verification = alternate_verifier.verify_election_partial_key_challenge( challenge ) # Assert self.assertIsNotNone(verification) self.assertEqual(verification.owner_id, guardian.id) self.assertEqual(verification.designated_id, other_guardian.id) self.assertEqual(verification.verifier_id, alternate_verifier.id) self.assertTrue(verification.verified) def test_publish_election_backup_challenge(self) -> None: # Arrange guardian = Guardian.from_nonce( SENDER_GUARDIAN_ID, SENDER_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM ) other_guardian = Guardian.from_nonce( RECIPIENT_GUARDIAN_ID, RECIPIENT_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM ) guardian.save_guardian_key(other_guardian.share_key()) guardian.generate_election_partial_key_backups() # Act challenge = guardian.publish_election_backup_challenge(other_guardian.id) # Assert self.assertIsNotNone(challenge) self.assertIsNotNone(challenge.value) self.assertEqual(challenge.owner_id, guardian.id) self.assertEqual(challenge.designated_id, other_guardian.id) self.assertEqual(len(challenge.coefficient_commitments), QUORUM) self.assertEqual(len(challenge.coefficient_proofs), QUORUM) for proof in challenge.coefficient_proofs: proof.is_valid() def test_save_election_partial_key_verification(self) -> None: # Arrange # Round 1 guardian = Guardian.from_nonce( SENDER_GUARDIAN_ID, SENDER_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM ) other_guardian = Guardian.from_nonce( RECIPIENT_GUARDIAN_ID, RECIPIENT_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM ) guardian.save_guardian_key(other_guardian.share_key()) other_guardian.save_guardian_key(guardian.share_key()) # Round 2 guardian.generate_election_partial_key_backups() key_backup = guardian.share_election_partial_key_backup(RECIPIENT_GUARDIAN_ID) other_guardian.save_election_partial_key_backup(key_backup) verification = other_guardian.verify_election_partial_key_backup( SENDER_GUARDIAN_ID, ) # Act guardian.save_election_partial_key_verification(verification) # Assert self.assertTrue(guardian.all_election_partial_key_backups_verified) def test_all_election_partial_key_backups_verified(self) -> None: # Arrange # Round 1 guardian = Guardian.from_nonce( SENDER_GUARDIAN_ID, SENDER_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM ) other_guardian = Guardian.from_nonce( RECIPIENT_GUARDIAN_ID, RECIPIENT_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM ) guardian.save_guardian_key(other_guardian.share_key()) other_guardian.save_guardian_key(guardian.share_key()) # Round 2 guardian.generate_election_partial_key_backups() key_backup = guardian.share_election_partial_key_backup(other_guardian.id) other_guardian.save_election_partial_key_backup(key_backup) verification = other_guardian.verify_election_partial_key_backup( guardian.id, ) guardian.save_election_partial_key_verification(verification) # Act all_saved = guardian.all_election_partial_key_backups_verified() # Assert self.assertTrue(all_saved) def test_publish_joint_key(self) -> None: # Arrange # Round 1 guardian = Guardian.from_nonce( SENDER_GUARDIAN_ID, SENDER_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM ) other_guardian = Guardian.from_nonce( RECIPIENT_GUARDIAN_ID, RECIPIENT_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM ) guardian.save_guardian_key(other_guardian.share_key()) other_guardian.save_guardian_key(guardian.share_key()) # Round 2 guardian.generate_election_partial_key_backups() key_backup = guardian.share_election_partial_key_backup(other_guardian.id) other_guardian.save_election_partial_key_backup(key_backup) verification = other_guardian.verify_election_partial_key_backup( guardian.id, ) # Act joint_key = guardian.publish_joint_key() # Assert self.assertIsNone(joint_key) # Act guardian.save_guardian_key(other_guardian.share_key()) joint_key = guardian.publish_joint_key() # Assert self.assertIsNone(joint_key) # Act guardian.save_election_partial_key_verification(verification) joint_key = guardian.publish_joint_key() # Assert self.assertIsNotNone(joint_key) self.assertNotEqual(joint_key, guardian.share_key().key) ================================================ FILE: tests/unit/electionguard/test_hmac.py ================================================ from electionguard.hmac import get_hmac from tests.base_test_case import BaseTestCase class TestHmac(BaseTestCase): """HMAC tests""" def test_get_hmac(self) -> None: """ Validate that hmac can be generated correctly. """ # Arrange key = b"mock_key" message = b"mock_message" length = 256 start = 1 # Act hmac_1 = get_hmac(key, message) hmac_2 = get_hmac(key, message, length) hmac_3 = get_hmac(key, message, length, start) # Assert self.assertIsNotNone(hmac_1) self.assertIsNotNone(hmac_2) self.assertIsNotNone(hmac_3) ================================================ FILE: tests/unit/electionguard/test_key_ceremony.py ================================================ import os from unittest.mock import patch from tests.base_test_case import BaseTestCase from electionguard.constants import ( PrimeOption, ) from electionguard.key_ceremony import ( generate_election_key_pair, generate_election_partial_key_backup, verify_election_partial_key_backup, generate_election_partial_key_challenge, verify_election_partial_key_challenge, combine_election_public_keys, ) NUMBER_OF_GUARDIANS = 5 QUORUM = 3 SENDER_GUARDIAN_ID = "Test Guardian 1" SENDER_SEQUENCE_ORDER = 1 RECIPIENT_GUARDIAN_ID = "Test Guardian 2" RECIPIENT_SEQUENCE_ORDER = 2 RECIPIENT_KEY_PAIR = generate_election_key_pair( RECIPIENT_GUARDIAN_ID, RECIPIENT_SEQUENCE_ORDER, QUORUM ) RECIPIENT_KEY = RECIPIENT_KEY_PAIR.share() ALTERNATE_VERIFIER_GUARDIAN_ID = "Test Guardian 3" @patch.dict(os.environ, {"PRIME_OPTION": PrimeOption.Standard.value}) class TestKeyCeremony(BaseTestCase): """Key ceremony tests""" def test_generate_election_key_pair(self) -> None: # Act election_key_pair = generate_election_key_pair( SENDER_GUARDIAN_ID, SENDER_SEQUENCE_ORDER, QUORUM ) # Assert self.assertIsNotNone(election_key_pair) self.assertIsNotNone(election_key_pair.key_pair.public_key) self.assertIsNotNone(election_key_pair.key_pair.secret_key) self.assertIsNotNone(election_key_pair.polynomial) self.assertEqual(len(election_key_pair.polynomial.coefficients), QUORUM) for coefficient in election_key_pair.polynomial.coefficients: self.assertTrue(coefficient.proof.is_valid()) def test_generate_election_partial_key_backup(self) -> None: # Arrange election_key_pair = generate_election_key_pair( SENDER_GUARDIAN_ID, SENDER_SEQUENCE_ORDER, QUORUM ) # Act backup = generate_election_partial_key_backup( SENDER_GUARDIAN_ID, election_key_pair.polynomial, RECIPIENT_KEY, ) # Assert self.assertIsNotNone(backup) self.assertEqual(backup.designated_id, RECIPIENT_GUARDIAN_ID) self.assertEqual(backup.designated_sequence_order, RECIPIENT_SEQUENCE_ORDER) self.assertIsNotNone(backup.encrypted_coordinate) def test_encrypt_then_verify_coordinate(self) -> None: # Arrange sender_key_pair = generate_election_key_pair( SENDER_GUARDIAN_ID, SENDER_SEQUENCE_ORDER, QUORUM ) # Act partial_key_backup = generate_election_partial_key_backup( SENDER_GUARDIAN_ID, sender_key_pair.polynomial, RECIPIENT_KEY, ) verification = verify_election_partial_key_backup( RECIPIENT_GUARDIAN_ID, partial_key_backup, sender_key_pair.share(), RECIPIENT_KEY_PAIR, ) # Assert self.assertTrue(verification.verified) def test_verify_election_partial_key_backup(self) -> None: # Arrange sender_election_key_pair = generate_election_key_pair( SENDER_GUARDIAN_ID, SENDER_SEQUENCE_ORDER, QUORUM ) partial_key_backup = generate_election_partial_key_backup( SENDER_GUARDIAN_ID, sender_election_key_pair.polynomial, RECIPIENT_KEY, ) # Act verification = verify_election_partial_key_backup( RECIPIENT_GUARDIAN_ID, partial_key_backup, sender_election_key_pair.share(), RECIPIENT_KEY_PAIR, ) # Assert self.assertIsNotNone(verification) self.assertEqual(verification.owner_id, SENDER_GUARDIAN_ID) self.assertEqual(verification.designated_id, RECIPIENT_GUARDIAN_ID) self.assertEqual(verification.verifier_id, RECIPIENT_GUARDIAN_ID) self.assertTrue(verification.verified) def test_generate_election_partial_key_challenge(self) -> None: # Arrange sender_election_key_pair = generate_election_key_pair( SENDER_GUARDIAN_ID, SENDER_SEQUENCE_ORDER, QUORUM ) partial_key_backup = generate_election_partial_key_backup( SENDER_GUARDIAN_ID, sender_election_key_pair.polynomial, RECIPIENT_KEY, ) # Act challenge = generate_election_partial_key_challenge( partial_key_backup, sender_election_key_pair.polynomial ) # Assert self.assertIsNotNone(challenge) self.assertEqual(challenge.designated_id, RECIPIENT_GUARDIAN_ID) self.assertEqual(challenge.designated_sequence_order, RECIPIENT_SEQUENCE_ORDER) self.assertIsNotNone(challenge.value) self.assertEqual(len(challenge.coefficient_commitments), QUORUM) self.assertEqual(len(challenge.coefficient_proofs), QUORUM) for proof in challenge.coefficient_proofs: self.assertTrue(proof.is_valid()) def test_verify_election_partial_key_challenge(self) -> None: # Arrange sender_election_key_pair = generate_election_key_pair( SENDER_GUARDIAN_ID, SENDER_SEQUENCE_ORDER, QUORUM ) partial_key_backup = generate_election_partial_key_backup( SENDER_GUARDIAN_ID, sender_election_key_pair.polynomial, RECIPIENT_KEY, ) challenge = generate_election_partial_key_challenge( partial_key_backup, sender_election_key_pair.polynomial ) # Act verification = verify_election_partial_key_challenge( ALTERNATE_VERIFIER_GUARDIAN_ID, challenge ) # Assert self.assertIsNotNone(verification) self.assertEqual(verification.owner_id, SENDER_GUARDIAN_ID) self.assertEqual(verification.designated_id, RECIPIENT_GUARDIAN_ID) self.assertEqual(verification.verifier_id, ALTERNATE_VERIFIER_GUARDIAN_ID) self.assertTrue(verification.verified) def test_combine_election_public_keys(self) -> None: # Arrange random_key = generate_election_key_pair( RECIPIENT_GUARDIAN_ID, SENDER_SEQUENCE_ORDER, QUORUM ).share() random_key_two = generate_election_key_pair( SENDER_GUARDIAN_ID, RECIPIENT_SEQUENCE_ORDER, QUORUM ).share() # Act joint_key = combine_election_public_keys([random_key, random_key_two]) # Assert self.assertIsNotNone(joint_key) self.assertNotEqual(joint_key.joint_public_key, random_key.key) self.assertNotEqual(joint_key.joint_public_key, random_key_two.key) ================================================ FILE: tests/unit/electionguard/test_key_ceremony_mediator.py ================================================ from typing import List from tests.base_test_case import BaseTestCase from electionguard.guardian import Guardian from electionguard.key_ceremony import ( CeremonyDetails, ElectionPartialKeyVerification, ) from electionguard.key_ceremony_mediator import KeyCeremonyMediator, GuardianPair from electionguard_tools.helpers.key_ceremony_orchestrator import ( KeyCeremonyOrchestrator, ) NUMBER_OF_GUARDIANS = 2 QUORUM = 2 CEREMONY_DETAILS = CeremonyDetails(NUMBER_OF_GUARDIANS, QUORUM) GUARDIAN_1_ID = "Guardian 1" GUARDIAN_2_ID = "Guardian 2" class TestKeyCeremonyMediator(BaseTestCase): """Key ceremony mediator tests""" GUARDIAN_1: Guardian GUARDIAN_2: Guardian GUARDIANS: List[Guardian] = [] def setUp(self) -> None: super().setUp() self.GUARDIAN_1 = Guardian.from_nonce( GUARDIAN_1_ID, 1, NUMBER_OF_GUARDIANS, QUORUM ) self.GUARDIAN_2 = Guardian.from_nonce( GUARDIAN_2_ID, 2, NUMBER_OF_GUARDIANS, QUORUM ) self.GUARDIANS = [self.GUARDIAN_1, self.GUARDIAN_2] def test_reset(self) -> None: # Arrange mediator = KeyCeremonyMediator("mediator_reset", CEREMONY_DETAILS) new_ceremony_details = CeremonyDetails(3, 3) mediator.reset(new_ceremony_details) self.assertEqual(mediator.ceremony_details, new_ceremony_details) def test_take_attendance(self) -> None: """Round 1: Mediator takes attendance and guardians announce""" # Arrange mediator = KeyCeremonyMediator("mediator_attendance", CEREMONY_DETAILS) # Act mediator.announce(self.GUARDIAN_1.share_key()) # Assert self.assertFalse(mediator.all_guardians_announced()) # Act mediator.announce(self.GUARDIAN_2.share_key()) # Assert self.assertTrue(mediator.all_guardians_announced()) # Act guardian_key_sets = mediator.share_announced() # Assert self.assertIsNotNone(guardian_key_sets) self.assertEqual(len(guardian_key_sets), NUMBER_OF_GUARDIANS) def test_exchange_of_backups(self) -> None: """Round 2: Exchange of election partial key backups""" # Arrange mediator = KeyCeremonyMediator("mediator_backups_exchange", CEREMONY_DETAILS) KeyCeremonyOrchestrator.perform_round_1(self.GUARDIANS, mediator) # Round 2 - Guardians Only self.GUARDIAN_1.generate_election_partial_key_backups() self.GUARDIAN_2.generate_election_partial_key_backups() backup_from_1_for_2 = self.GUARDIAN_1.share_election_partial_key_backup( GUARDIAN_2_ID ) backup_from_2_for_1 = self.GUARDIAN_2.share_election_partial_key_backup( GUARDIAN_1_ID ) # Act mediator.receive_backups([backup_from_1_for_2]) # Assert self.assertFalse(mediator.all_backups_available()) # Act mediator.receive_backups([backup_from_2_for_1]) # Assert self.assertTrue(mediator.all_backups_available()) # Act guardian1_backups = mediator.share_backups(GUARDIAN_1_ID) guardian2_backups = mediator.share_backups(GUARDIAN_2_ID) # Assert self.assertIsNotNone(guardian1_backups) self.assertIsNotNone(guardian2_backups) self.assertEqual(len(guardian1_backups), 1) self.assertEqual(len(guardian2_backups), 1) for backup in guardian1_backups: self.assertEqual(backup.designated_id, GUARDIAN_1_ID) for backup in guardian2_backups: self.assertEqual(backup.designated_id, GUARDIAN_2_ID) self.assertEqual(guardian1_backups[0], backup_from_2_for_1) self.assertEqual(guardian2_backups[0], backup_from_1_for_2) # Partial Key Verifications def test_partial_key_backup_verification_success(self) -> None: """ Test for the happy path of the verification process where each key is successfully verified and no bad actors. """ # Arrange mediator = KeyCeremonyMediator("mediator_verification", CEREMONY_DETAILS) KeyCeremonyOrchestrator.perform_round_1(self.GUARDIANS, mediator) KeyCeremonyOrchestrator.perform_round_2(self.GUARDIANS, mediator) # Round 3 - Guardians only verification1 = self.GUARDIAN_1.verify_election_partial_key_backup( GUARDIAN_2_ID, ) verification2 = self.GUARDIAN_2.verify_election_partial_key_backup( GUARDIAN_1_ID, ) # Act mediator.receive_backup_verifications([verification1]) # Assert self.assertFalse(mediator.get_verification_state().all_sent) self.assertFalse(mediator.all_backups_verified()) self.assertIsNone(mediator.publish_joint_key()) # Act mediator.receive_backup_verifications([verification2]) joint_key = mediator.publish_joint_key() # Assert self.assertTrue(mediator.get_verification_state().all_sent) self.assertTrue(mediator.all_backups_verified()) self.assertIsNotNone(joint_key) def test_partial_key_backup_verification_failure(self) -> None: """ In this case, the recipient guardian does not correctly verify the sent key backup. This failed verificaton requires the sender create a challenge and a new verifier aka another guardian must verify this challenge. """ # Arrange mediator = KeyCeremonyMediator("mediator_challenge", CEREMONY_DETAILS) KeyCeremonyOrchestrator.perform_round_1(self.GUARDIANS, mediator) KeyCeremonyOrchestrator.perform_round_2(self.GUARDIANS, mediator) # Round 3 - Guardians only verification1 = self.GUARDIAN_1.verify_election_partial_key_backup( GUARDIAN_2_ID, ) # Act failed_verification2 = ElectionPartialKeyVerification( GUARDIAN_1_ID, GUARDIAN_2_ID, GUARDIAN_2_ID, False, ) mediator.receive_backup_verifications([verification1, failed_verification2]) state = mediator.get_verification_state() # Assert self.assertTrue(state.all_sent) self.assertFalse(state.all_verified) self.assertIsNone(mediator.publish_joint_key()) self.assertEqual(len(state.failed_verifications), 1) self.assertEqual( state.failed_verifications[0], GuardianPair(GUARDIAN_1_ID, GUARDIAN_2_ID) ) # Act challenge = self.GUARDIAN_1.publish_election_backup_challenge(GUARDIAN_2_ID) mediator.verify_challenge(challenge) new_state = mediator.get_verification_state() all_verified = mediator.all_backups_verified() joint_key = mediator.publish_joint_key() # Assert self.assertTrue(new_state.all_sent) self.assertTrue(new_state.all_verified) self.assertEqual(len(new_state.failed_verifications), 0) self.assertTrue(all_verified) self.assertIsNotNone(joint_key) ================================================ FILE: tests/unit/electionguard/test_logs.py ================================================ import logging from tests.base_test_case import BaseTestCase from electionguard.logs import ( get_stream_handler, log_add_handler, log_remove_handler, log_handlers, log_debug, log_error, log_info, log_warning, ) class TestLogs(BaseTestCase): """Logging tests""" def test_log_methods(self): # Arrange message = "test log message" # Act log_debug(message) log_error(message) log_info(message) log_warning(message) # Assert self.assertIsNotNone(message) def test_log_handlers(self): # Arrange # Act handlers = log_handlers() # Assert self.assertEqual(len(handlers), 1) # Act log_remove_handler(handlers[0]) empty_handlers = log_handlers() # Assert self.assertEqual(len(empty_handlers), 0) # Act log_add_handler(get_stream_handler(logging.INFO)) added_handlers = log_handlers() # Assert self.assertEqual(len(added_handlers), 1) ================================================ FILE: tests/unit/electionguard/test_manifest.py ================================================ from dataclasses import dataclass from datetime import datetime from tests.base_test_case import BaseTestCase from electionguard.manifest import ( Candidate, ContestDescription, ContestDescriptionWithPlaceholders, InternationalizedText, Language, Manifest, InternalManifest, SelectionDescription, VoteVariationType, ) from electionguard.serialize import from_raw, to_raw import electionguard_tools.factories.election_factory as ElectionFactory import electionguard_tools.factories.ballot_factory as BallotFactory election_factory = ElectionFactory.ElectionFactory() ballot_factory = BallotFactory.BallotFactory() class TestManifest(BaseTestCase): """Manifest tests""" @staticmethod def _set_selection( manifest: Manifest, selection_id: str, candidate_id: str ) -> None: selection = SelectionDescription(selection_id, 1, candidate_id) contest = ContestDescription( "contest1", 1, "e1", VoteVariationType.approval, 1, 1, "contest", [selection], ) manifest.contests = [contest] @staticmethod def _set_candidate( manifest: Manifest, candidate_id: str, name: str, lang: str, write_in: bool = False, ) -> None: text_name = InternationalizedText([Language(name, lang)]) candidate = Candidate(candidate_id, text_name, is_write_in=write_in) manifest.candidates = [candidate] def test_get_selection_names_with_valid_selection(self) -> None: # arrange manifest = election_factory.get_simple_manifest_from_file() TestManifest._set_selection(manifest, "selection1", "candidate1") TestManifest._set_candidate(manifest, "candidate1", "My Candidate", "en") # act selection_names = manifest.get_selection_names("en") # assert self.assertEqual(1, len(selection_names.keys())) self.assertEqual("My Candidate", selection_names["selection1"]) def test_get_selection_names_with_missing_language(self) -> None: # arrange manifest = election_factory.get_simple_manifest_from_file() TestManifest._set_selection(manifest, "selection1", "candidate1") TestManifest._set_candidate(manifest, "candidate1", "My Candidate", "en") # act selection_names = manifest.get_selection_names("es") # assert self.assertEqual(1, len(selection_names.keys())) self.assertEqual("candidate1", selection_names["selection1"]) def test_get_selection_names_with_missing_candidate(self) -> None: # arrange manifest = election_factory.get_simple_manifest_from_file() TestManifest._set_selection(manifest, "selection1", "candidate1") manifest.candidates = [] # act selection_names = manifest.get_selection_names("es") # assert self.assertEqual(1, len(selection_names.keys())) self.assertEqual("candidate1", selection_names["selection1"]) def test_get_selection_names_with_write_in(self) -> None: # arrange manifest = election_factory.get_simple_manifest_from_file() TestManifest._set_selection(manifest, "selection1", "candidate1") TestManifest._set_candidate(manifest, "candidate1", "", "en", write_in=True) # act selection_names = manifest.get_selection_names("es") # assert self.assertEqual(1, len(selection_names.keys())) self.assertEqual("Write-In", selection_names["selection1"]) def test_simple_manifest_is_valid(self) -> None: # Act subject = election_factory.get_simple_manifest_from_file() # Assert self.assertIsNotNone(subject.election_scope_id) self.assertEqual(subject.election_scope_id, "jefferson-county-primary") self.assertTrue(subject.is_valid()) def test_simple_manifest_can_serialize(self) -> None: # Arrange subject = election_factory.get_simple_manifest_from_file() intermediate = to_raw(subject) # Act result = from_raw(Manifest, intermediate) # Assert self.assertIsNotNone(result.election_scope_id) self.assertEqual(result.election_scope_id, "jefferson-county-primary") def test_manifest_has_deterministic_hash(self) -> None: # Act subject1 = election_factory.get_simple_manifest_from_file() subject2 = election_factory.get_simple_manifest_from_file() # Assert self.assertEqual(subject1.crypto_hash(), subject2.crypto_hash()) def test_manifest_hash_is_consistent_regardless_of_format(self) -> None: # Act @dataclass class DateType: """Temp date class for testing""" date: datetime subject1 = election_factory.get_simple_manifest_from_file() subject1.start_date = from_raw( DateType, '{"date":"2020-03-01T08:00:00-05:00"}' ).date subject2 = election_factory.get_simple_manifest_from_file() subject2.start_date = from_raw( DateType, '{"date":"2020-03-01T13:00:00-00:00"}' ).date subject3 = election_factory.get_simple_manifest_from_file() subject3.start_date = from_raw( DateType, '{"date":"2020-03-01T13:00:00.000-00:00"}' ).date subject4 = election_factory.get_simple_manifest_from_file() subject4.start_date = from_raw(DateType, '{"date":"2020-03-01T13:00:00Z"}').date subjects = [subject1, subject2, subject3, subject4] # Assert hashes = [subject.crypto_hash() for subject in subjects] for other_hash in hashes[1:]: self.assertEqual(hashes[0], other_hash) def test_manifest_from_file_generates_consistent_internal_description_contest_hashes( self, ) -> None: # Arrange comparator = election_factory.get_simple_manifest_from_file() subject = InternalManifest(comparator) self.assertEqual(len(comparator.contests), len(subject.contests)) for expected in comparator.contests: for actual in subject.contests: if expected.object_id == actual.object_id: self.assertEqual(expected.crypto_hash(), actual.crypto_hash()) def test_contest_description_valid_input_succeeds(self) -> None: description = ContestDescriptionWithPlaceholders( object_id="0@A.com-contest", electoral_district_id="0@A.com-gp-unit", sequence_order=1, vote_variation=VoteVariationType.n_of_m, number_elected=1, votes_allowed=1, name="", ballot_selections=[ SelectionDescription( "0@A.com-selection", 0, "0@A.com", ), SelectionDescription( "0@B.com-selection", 1, "0@B.com", ), ], ballot_title=None, ballot_subtitle=None, placeholder_selections=[ SelectionDescription( "0@A.com-contest-2-placeholder", 2, "0@A.com-contest-2-candidate", ) ], ) self.assertTrue(description.is_valid()) def test_contest_description_invalid_input_fails(self) -> None: description = ContestDescriptionWithPlaceholders( object_id="0@A.com-contest", electoral_district_id="0@A.com-gp-unit", sequence_order=1, vote_variation=VoteVariationType.n_of_m, number_elected=1, votes_allowed=1, name="", ballot_selections=[ SelectionDescription( "0@A.com-selection", 0, "0@A.com", ), # simulate a bad selection description input SelectionDescription( "0@A.com-selection", 1, "0@A.com", ), ], ballot_title=None, ballot_subtitle=None, placeholder_selections=[ SelectionDescription( "0@A.com-contest-2-placeholder", 2, "0@A.com-contest-2-candidate", ) ], ) self.assertFalse(description.is_valid()) ================================================ FILE: tests/unit/electionguard/test_scheduler.py ================================================ # pylint: disable=consider-using-with from multiprocessing import Pool from typing import List from tests.base_test_case import BaseTestCase from electionguard.scheduler import Scheduler def _callable(data: int): return data def _exception_callable(something: int): raise Exception class TestScheduler(BaseTestCase): """Scheduler tests""" def test_schedule_callable_throws(self): # Arrange subject = Scheduler() # Act result = subject.schedule(_exception_callable, [list([1]), list([2])]) # Assert self.assertIsNotNone(result) self.assertIsInstance(result, List) subject.close() def test_safe_map(self): # Arrange process_pool = Pool(1) subject = Scheduler() # Act result = subject.safe_map(process_pool, _callable, [1]) self.assertEqual(result, [1]) # verify exceptions are caught result = subject.safe_map(process_pool, _exception_callable, [1]) self.assertIsNotNone(result) self.assertIsInstance(result, List) # verify closing the poll catches the value error process_pool.close() result = subject.safe_map(process_pool, _callable, [1]) self.assertIsNotNone(result) self.assertIsInstance(result, List) subject.close() ================================================ FILE: tests/unit/electionguard/test_singleton.py ================================================ from tests.base_test_case import BaseTestCase from electionguard.singleton import Singleton class TestSingleton(BaseTestCase): """Singleton tests""" def test_singleton(self): singleton = Singleton() same_instance = singleton.get_instance() self.assertIsNotNone(singleton) self.assertIsNotNone(same_instance) def test_singleton_when_not_initialized(self): instance = Singleton.get_instance() self.assertIsNotNone(instance) ================================================ FILE: tests/unit/electionguard/test_utils.py ================================================ from datetime import datetime, timezone from tests.base_test_case import BaseTestCase from electionguard.utils import to_ticks class TestUtils(BaseTestCase): """Utility tests""" def test_conversion_to_ticks_from_utc(self): # Act ticks = to_ticks(datetime.now(timezone.utc)) self.assertIsNotNone(ticks) self.assertGreater(ticks, 0) def test_conversion_to_ticks_from_local(self): # Act ticks = to_ticks(datetime.now()) self.assertIsNotNone(ticks) self.assertGreater(ticks, 0) def test_conversion_to_ticks_with_tz(self): # Arrange now = datetime.now() now_with_tz = (now).astimezone() now_utc_with_tz = now_with_tz.astimezone(timezone.utc) # Act ticks_now = to_ticks(now) ticks_local = to_ticks(now_with_tz) ticks_utc = to_ticks(now_utc_with_tz) # Assert self.assertIsNotNone(ticks_now) self.assertIsNotNone(ticks_local) self.assertIsNotNone(ticks_utc) # Ensure all three tick values are the same unique_ticks = set([ticks_now, ticks_local, ticks_utc]) self.assertEqual(1, len(unique_ticks)) self.assertTrue(ticks_now in unique_ticks) def test_conversion_to_ticks_produces_valid_epoch(self): # Arrange now = datetime.now() # Act ticks_now = to_ticks(now) now_from_ticks = datetime.fromtimestamp(ticks_now) # Assert # Values below seconds are dropped from the epoch self.assertEqual(now.replace(microsecond=0), now_from_ticks) ================================================ FILE: tests/unit/electionguard_gui/__init__.py ================================================ ================================================ FILE: tests/unit/electionguard_gui/test_decryption_dto.py ================================================ from datetime import datetime, timezone from unittest.mock import MagicMock, patch from electionguard_gui.models.decryption_dto import DecryptionDto from tests.base_test_case import BaseTestCase class TestDecryptionDto(BaseTestCase): """Test the DecryptionDto class""" def test_no_spoiled_ballots(self) -> None: # ARRANGE decryption_dto = DecryptionDto({}) # ACT spoiled_ballots = decryption_dto.get_plaintext_spoiled_ballots() # ASSERT self.assertEqual(0, len(spoiled_ballots)) def test_get_status_with_no_guardians(self) -> None: # ARRANGE decryption_dto = DecryptionDto( { "guardians_joined": [], "guardians": 2, } ) # ACT status = decryption_dto.get_status() # ASSERT self.assertEqual(status, "waiting for all guardians to join") def test_get_status_with_all_guardians_joined_but_not_completed(self) -> None: # ARRANGE decryption_dto = DecryptionDto( {"guardians_joined": ["g1", "g2"], "guardians": 2, "completed_at_utc": None} ) # ACT status = decryption_dto.get_status() # ASSERT self.assertEqual(status, "performing decryption") def test_get_status_with_all_guardians_joined_and_completed(self) -> None: # ARRANGE decryption_dto = DecryptionDto( { "guardians_joined": ["g1"], "guardians": 1, "completed_at": datetime.now(timezone.utc), } ) # ACT status = decryption_dto.get_status() # ASSERT self.assertEqual(status, "decryption complete") @patch("electionguard_gui.services.authorization_service.AuthorizationService") def test_admins_can_not_join_key_ceremony(self, auth_service: MagicMock) -> None: # ARRANGE decryption_dto = DecryptionDto({"guardians_joined": []}) auth_service.configure_mock( **{"get_user_id.return_value": "admin1", "is_admin.return_value": True} ) # ACT decryption_dto.set_can_join(auth_service) # ASSERT self.assertFalse(decryption_dto.can_join) @patch("electionguard_gui.services.authorization_service.AuthorizationService") def test_users_can_join_key_ceremony_if_not_already_joined( self, auth_service: MagicMock ) -> None: # ARRANGE decryption_dto = DecryptionDto({"guardians_joined": []}) auth_service.configure_mock( **{"get_user_id.return_value": "user1", "is_admin.return_value": False} ) # ACT decryption_dto.set_can_join(auth_service) # ASSERT self.assertTrue(decryption_dto.can_join) @patch("electionguard_gui.services.authorization_service.AuthorizationService") def test_users_cant_join_twice(self, auth_service: MagicMock) -> None: # ARRANGE decryption_dto = DecryptionDto({"guardians_joined": ["user1"]}) auth_service.configure_mock( **{"get_user_id.return_value": "user1", "is_admin.return_value": False} ) # ACT decryption_dto.set_can_join(auth_service) # ASSERT self.assertFalse(decryption_dto.can_join) ================================================ FILE: tests/unit/electionguard_gui/test_eel_utils.py ================================================ from datetime import datetime, timezone from electionguard_gui.eel_utils import utc_to_str from tests.base_test_case import BaseTestCase class TestEelUtils(BaseTestCase): """Tests eel utils""" def test_utc_to_str_with_valid_utc_date(self): date = datetime(2020, 2, 3, 7, 10, 10, 0, tzinfo=timezone.utc) result = utc_to_str(date) # this test may be run in different timezones, so we can't test for exact time self.assertRegex(result, "Feb 3, 2020 [0-9]:10 AM") def test_utc_to_str_with_empty(self): result = utc_to_str(None) self.assertEqual(result, "") ================================================ FILE: tests/unit/electionguard_gui/test_election_dto.py ================================================ from datetime import datetime, timezone from electionguard_gui.models.election_dto import ElectionDto from tests.base_test_case import BaseTestCase class TestElectionDto(BaseTestCase): """Test the ElectionDto class""" def test_get_status_with_no_guardians(self) -> None: # ARRANGE self.mocker.patch( "electionguard_gui.models.election_dto.utc_to_str", return_value="Feb 3, 2022 2:10 PM", ) election_dto = ElectionDto( { "_id": "ABC", "created_at": datetime(2020, 2, 3, 7, 10, 10, 0, tzinfo=timezone.utc), } ) # ACT result = election_dto.to_dict() # ASSERT self.assertEqual("ABC", result["id"]) self.assertEqual("Feb 3, 2022 2:10 PM", result["created_at"]) def test_sum_two_ballots(self) -> None: # ARRANGE election_dto = ElectionDto( { "ballot_uploads": [ { "ballot_count": 1, "ballot_type": "ballot_type_1", }, { "ballot_count": 2, "ballot_type": "ballot_type_2", }, ] } ) # ACT result = election_dto.sum_ballots() # ASSERT self.assertEqual(3, result) def test_sum_zero_ballots(self) -> None: # ARRANGE election_dto = ElectionDto({"ballot_uploads": []}) # ACT result = election_dto.sum_ballots() # ASSERT self.assertEqual(0, result) ================================================ FILE: tests/unit/electionguard_gui/test_plaintext_ballot_service.py ================================================ from unittest.mock import MagicMock, patch from electionguard.tally import PlaintextTally, PlaintextTallySelection from electionguard_gui.services.plaintext_ballot_service import ( _get_contest_details, _get_tally_report, ) from tests.base_test_case import BaseTestCase class TestPlaintextBallotService(BaseTestCase): """Test the ElectionDto class""" def test_get_tally_report_with_no_contests(self) -> None: # ARRANGE plaintext_ballot = PlaintextTally("tally", {}) selection_names: dict[str, str] = {} selection_write_ins: dict[str, bool] = {} parties: dict[str, str] = {} contest_names: dict[str, str] = {} # ACT result = _get_tally_report( plaintext_ballot, selection_names, contest_names, selection_write_ins, parties, ) # ASSERT self.assertEqual(0, len(result)) @patch("electionguard.tally.PlaintextTallySelection") def test_given_one_contest_with_valid_name_when_get_tally_report_then_name_returned( self, plaintext_tally_selection: MagicMock ) -> None: # ARRANGE plaintext_tally_selection.object_id = "c-1" plaintext_ballot = PlaintextTally("tally", {"c-1": plaintext_tally_selection}) selection_names: dict[str, str] = {} selection_write_ins: dict[str, bool] = {} parties: dict[str, str] = {} contest_names: dict[str, str] = {"c-1": "Contest 1"} # ACT result = _get_tally_report( plaintext_ballot, selection_names, contest_names, selection_write_ins, parties, ) # ASSERT self.assertEqual(1, len(result)) self.assertEqual("Contest 1", result[0]["name"]) @patch("electionguard.tally.PlaintextTallySelection") def test_given_one_contest_with_invalid_name_when_get_tally_report_then_name_is_na( self, plaintext_tally_selection: MagicMock ) -> None: # ARRANGE plaintext_tally_selection.object_id = "c-1" plaintext_ballot = PlaintextTally("tally", {"c-1": plaintext_tally_selection}) selection_names: dict[str, str] = {} selection_write_ins: dict[str, bool] = {} parties: dict[str, str] = {} contest_names: dict[str, str] = {} # ACT result = _get_tally_report( plaintext_ballot, selection_names, contest_names, selection_write_ins, parties, ) # ASSERT self.assertEqual(1, len(result)) self.assertEqual("n/a", list(result)[0]["name"]) @patch("electionguard.tally.PlaintextTallySelection") @patch("electionguard.tally.PlaintextTallySelection") def test_given_two_contests_with_duplicate_names_when_get_tally_report_then_both_names_returned( self, plaintext_tally_selection1: MagicMock, plaintext_tally_selection2: MagicMock, ) -> None: # ARRANGE plaintext_tally_selection1.object_id = "c-1" plaintext_tally_selection2.object_id = "c-2" plaintext_ballot = PlaintextTally( "tally", { "c-1": plaintext_tally_selection1, "c-2": plaintext_tally_selection2, }, ) selection_names: dict[str, str] = {} selection_write_ins: dict[str, bool] = {} parties: dict[str, str] = {} contest_names: dict[str, str] = {"c-1": "My Contest", "c-2": "My Contest"} # ACT result = _get_tally_report( plaintext_ballot, selection_names, contest_names, selection_write_ins, parties, ) # ASSERT self.assertEqual(2, len(result)) self.assertEqual("My Contest", list(result)[0]["name"]) self.assertEqual("My Contest", list(result)[1]["name"]) def test_zero_sections(self) -> None: # ARRANGE selections: list[PlaintextTallySelection] = [] selection_names: dict[str, str] = {} selection_write_ins: dict[str, bool] = {} parties: dict[str, str] = {} # ACT result = _get_contest_details( selections, selection_names, selection_write_ins, parties ) # ASSERT self.assertEqual(0, result["nonWriteInTotal"]) self.assertEqual(None, result["writeInTotal"]) self.assertEqual(0, len(result["selections"])) @patch("electionguard.tally.PlaintextTallySelection") def test_one_non_write_in(self, plaintext_tally_selection: MagicMock) -> None: # ARRANGE plaintext_tally_selection.object_id = "AL" plaintext_tally_selection.tally = 2 selections: list[PlaintextTallySelection] = [plaintext_tally_selection] selection_names: dict[str, str] = { "AL": "Abraham Lincoln", } selection_write_ins: dict[str, bool] = { "AL": False, } parties: dict[str, str] = { "AL": "National Union Party", } # ACT result = _get_contest_details( selections, selection_names, selection_write_ins, parties ) # ASSERT self.assertEqual(2, result["nonWriteInTotal"]) self.assertEqual(None, result["writeInTotal"]) self.assertEqual(1, len(result["selections"])) selection = result["selections"][0] self.assertEqual("Abraham Lincoln", selection["name"]) self.assertEqual(2, selection["tally"]) self.assertEqual("National Union Party", selection["party"]) self.assertEqual(1, selection["percent"]) @patch("electionguard.tally.PlaintextTallySelection") @patch("electionguard.tally.PlaintextTallySelection") def test_duplicate_section_names( self, plaintext_tally_selection1: MagicMock, plaintext_tally_selection2: MagicMock, ) -> None: # ARRANGE plaintext_tally_selection1.object_id = "S1" plaintext_tally_selection1.tally = 1 plaintext_tally_selection2.object_id = "S2" plaintext_tally_selection2.tally = 9 selections: list[PlaintextTallySelection] = [ plaintext_tally_selection1, plaintext_tally_selection2, ] selection_names: dict[str, str] = { "S1": "Abraham Lincoln", "S2": "Abraham Lincoln", } selection_write_ins: dict[str, bool] = { "S1": False, "S2": False, } parties: dict[str, str] = { "S1": "National Union Party", "S2": "National Union Party", } # ACT result = _get_contest_details( selections, selection_names, selection_write_ins, parties ) # ASSERT self.assertEqual(10, result["nonWriteInTotal"]) self.assertEqual(None, result["writeInTotal"]) self.assertEqual(2, len(result["selections"])) selection = result["selections"][0] self.assertEqual("Abraham Lincoln", selection["name"]) self.assertEqual(1, selection["tally"]) self.assertEqual("National Union Party", selection["party"]) self.assertEqual(0.1, selection["percent"]) selection = result["selections"][1] self.assertEqual("Abraham Lincoln", selection["name"]) self.assertEqual(9, selection["tally"]) self.assertEqual("National Union Party", selection["party"]) self.assertEqual(0.9, selection["percent"]) @patch("electionguard.tally.PlaintextTallySelection") def test_one_write_in(self, plaintext_tally_selection: MagicMock) -> None: # ARRANGE plaintext_tally_selection.object_id = "ST" plaintext_tally_selection.tally = 1 selections: list[PlaintextTallySelection] = [plaintext_tally_selection] selection_names: dict[str, str] = {} selection_write_ins: dict[str, bool] = { "ST": True, } parties: dict[str, str] = {} # ACT result = _get_contest_details( selections, selection_names, selection_write_ins, parties ) # ASSERT self.assertEqual(0, result["nonWriteInTotal"]) self.assertEqual(1, result["writeInTotal"]) self.assertEqual(0, len(result["selections"])) @patch("electionguard.tally.PlaintextTallySelection") def test_zero_write_in(self, plaintext_tally_selection: MagicMock) -> None: # ARRANGE plaintext_tally_selection.object_id = "ST" plaintext_tally_selection.tally = 0 selections: list[PlaintextTallySelection] = [plaintext_tally_selection] selection_names: dict[str, str] = {} selection_write_ins: dict[str, bool] = { "ST": True, } parties: dict[str, str] = {} # ACT result = _get_contest_details( selections, selection_names, selection_write_ins, parties ) # ASSERT self.assertEqual(0, result["nonWriteInTotal"]) self.assertEqual(0, result["writeInTotal"]) self.assertEqual(0, len(result["selections"]))