├── .github ├── dependabot.yml ├── label-actions.yml └── workflows │ ├── go.yml │ ├── issues-first-greet.yml │ ├── issues-label-actions.yml │ └── issues-stale.yml ├── .gitignore ├── .golangci.yml ├── .travis.yml ├── AUTHORS ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── SPEC.md ├── cmd ├── README.md ├── build ├── cfg-envelope.go ├── cfg-signet.go ├── cfg-tools.go ├── cmd-checksum.go ├── cmd-close.go ├── cmd-configure.go ├── cmd-generate.go ├── cmd-import-export.go ├── cmd-list.go ├── cmd-manage.go ├── cmd-open.go ├── cmd-sign.go ├── cmd-verify.go ├── cmd-version.go ├── format.go ├── format_sig.go ├── main.go ├── password.go ├── password_test.go ├── testdata │ ├── .truststore │ │ ├── 3911c84c-78f7-4354-a7f5-0e115aa2903c.recipient │ │ ├── 3911c84c-78f7-4354-a7f5-0e115aa2903c.signet │ │ └── safing-codesign-1.envelope │ ├── test.txt │ ├── test.txt.letter │ ├── test.txt.sig │ ├── test3.txt │ ├── test3.txt.sig │ ├── test4.txt │ └── testdir │ │ ├── test2.txt │ │ └── test2.txt.sig └── utils.go ├── core-wire.go ├── core-wire_test.go ├── core.go ├── core_test.go ├── defaults.go ├── doc.go ├── docs ├── AUDITS.md ├── audit_001_report_cure53_SAF-01.pdf ├── key_derivation.svg ├── key_establishment_dh.svg ├── key_establishment_dh.vp └── key_establishment_ke.svg ├── envelope.go ├── errors.go ├── filesig ├── format_armor.go ├── format_armor_test.go ├── helpers.go ├── json.go ├── json_test.go ├── main.go ├── main_test.go ├── text.go ├── text_test.go └── text_yaml.go ├── go.mod ├── go.sum ├── hashtools ├── blake2.go ├── blake3.go ├── hashtool.go ├── sha.go ├── tools.go └── tools_test.go ├── helper.go ├── import_export.go ├── letter-file.go ├── letter-wire.go ├── letter.go ├── letter_test.go ├── lhash ├── algs.go ├── labeledhash.go └── labeledhash_test.go ├── pack ├── password.go ├── password_test.go ├── random.go ├── requirements.go ├── requirements_test.go ├── session-wire.go ├── session.go ├── signet.go ├── suite.go ├── suites.go ├── suites_test.go ├── suites_v1.go ├── suites_v2.go ├── supply ├── supply.go └── supply_test.go ├── test ├── tools.go ├── tools ├── all │ └── all.go ├── blake3 │ └── kdf.go ├── ecdh │ ├── nist.go │ └── x25519.go ├── errors.go ├── gostdlib │ ├── aes-ctr.go │ ├── aes-gcm.go │ ├── chacha20-poly1305.go │ ├── ed25519.go │ ├── hkdf.go │ ├── hmac.go │ ├── pbkdf2.go │ ├── poly1305.go │ ├── rsa-keys.go │ ├── rsa-oaep.go │ ├── rsa-pss.go │ ├── salsa20.go │ └── scrypt.go ├── interfaces.go ├── tool.go ├── toollogic.go └── tools.go ├── tools_test.go ├── truststore.go └── truststores ├── dir.go ├── dir_test.go ├── extended.go ├── io.go ├── keyring.go └── utils.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /.github/label-actions.yml: -------------------------------------------------------------------------------- 1 | # Configuration for Label Actions - https://github.com/dessant/label-actions 2 | 3 | community support: 4 | comment: | 5 | Hey @{issue-author}, thank you for raising this issue with us. 6 | 7 | After a first review we noticed that this does not seem to be a technical issue, but rather a configuration issue or general question about how Portmaster works. 8 | 9 | Thus, we invite the community to help with configuration and/or answering this questions. 10 | 11 | If you are in a hurry or haven't received an answer, a good place to ask is in [our Discord community](https://discord.gg/safing). 12 | 13 | If your problem or question has been resolved or answered, please come back and give an update here for other users encountering the same and then close this issue. 14 | 15 | If you are a paying subscriber and want this issue to be checked out by Safing, please send us a message [on Discord](https://discord.gg/safing) or [via Email](mailto:support@safing.io) with your username and the link to this issue, so we can prioritize accordingly. 16 | 17 | needs debug info: 18 | comment: | 19 | Hey @{issue-author}, thank you for raising this issue with us. 20 | 21 | After a first review we noticed that we will require the Debug Info for further investigation. However, you haven't supplied any Debug Info in your report. 22 | 23 | Please [collect Debug Info](https://wiki.safing.io/en/FAQ/DebugInfo) from Portmaster _while_ the reported issue is present. 24 | 25 | in/compatibility: 26 | comment: | 27 | Hey @{issue-author}, thank you for reporting on a compatibility. 28 | 29 | We keep a list of compatible software and user provided guides for improving compatibility [in the wiki - please have a look there](https://wiki.safing.io/en/Portmaster/App/Compatibility). 30 | If you can't find your software in the list, then a good starting point is our guide on [How do I make software compatible with Portmaster](https://wiki.safing.io/en/FAQ/MakeSoftwareCompatibleWithPortmaster). 31 | 32 | If you have managed to establish compatibility with an application, please share your findings here. This will greatly help other users encountering the same issues. 33 | 34 | fixed: 35 | comment: | 36 | This issue has been fixed by the recently referenced commit or PR. 37 | 38 | However, the fix is not released yet. 39 | 40 | It is expected to go into the [Beta Release Channel](https://wiki.safing.io/en/FAQ/SwitchReleaseChannel) for testing within the next two weeks and will be available for everyone within the next four weeks. While this is the typical timeline we work with, things are subject to change. 41 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - develop 8 | pull_request: 9 | branches: 10 | - master 11 | - develop 12 | 13 | jobs: 14 | lint: 15 | name: Linter 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Check out code 19 | uses: actions/checkout@v3 20 | 21 | - name: Setup Go 22 | uses: actions/setup-go@v4 23 | with: 24 | go-version: '^1.19' 25 | 26 | - name: Get dependencies 27 | run: go mod download 28 | 29 | - name: Run golangci-lint 30 | uses: golangci/golangci-lint-action@v3 31 | with: 32 | version: v1.52.2 33 | only-new-issues: true 34 | args: -c ./.golangci.yml --timeout 15m 35 | 36 | - name: Run go vet 37 | run: go vet ./... 38 | 39 | test: 40 | name: Test 41 | runs-on: ubuntu-latest 42 | steps: 43 | - name: Check out code 44 | uses: actions/checkout@v3 45 | 46 | - name: Setup Go 47 | uses: actions/setup-go@v4 48 | with: 49 | go-version: '^1.19' 50 | 51 | - name: Get dependencies 52 | run: go mod download 53 | 54 | - name: Run tests 55 | run: ./test --test-only 56 | -------------------------------------------------------------------------------- /.github/workflows/issues-first-greet.yml: -------------------------------------------------------------------------------- 1 | # This workflow responds to first time posters with a greeting message. 2 | # Docs: https://github.com/actions/first-interaction 3 | name: Greet New Users 4 | 5 | # This workflow is triggered when a new issue is created. 6 | on: 7 | issues: 8 | types: opened 9 | 10 | permissions: 11 | contents: read 12 | issues: write 13 | 14 | jobs: 15 | greet: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/first-interaction@v1 19 | with: 20 | repo-token: ${{ secrets.GITHUB_TOKEN }} 21 | # Respond to first time issue raisers. 22 | issue-message: | 23 | Greetings and welcome to our community! As this is the first issue you opened here, we wanted to share some useful infos with you: 24 | 25 | - 🗣️ Our community on [Discord](https://discord.gg/safing) is super helpful and active. We also have an AI-enabled support bot that knows Portmaster well and can give you immediate help. 26 | - 📖 The [Wiki](https://wiki.safing.io/) answers all common questions and has many important details. If you can't find an answer there, let us know, so we can add anything that's missing. 27 | -------------------------------------------------------------------------------- /.github/workflows/issues-label-actions.yml: -------------------------------------------------------------------------------- 1 | # This workflow responds with a message when certain labels are added to an issue or PR. 2 | # Docs: https://github.com/dessant/label-actions 3 | name: Label Actions 4 | 5 | # This workflow is triggered when a label is added to an issue. 6 | on: 7 | issues: 8 | types: labeled 9 | 10 | permissions: 11 | contents: read 12 | issues: write 13 | 14 | jobs: 15 | action: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: dessant/label-actions@v3 19 | with: 20 | github-token: ${{ secrets.GITHUB_TOKEN }} 21 | config-path: ".github/label-actions.yml" 22 | process-only: "issues" 23 | -------------------------------------------------------------------------------- /.github/workflows/issues-stale.yml: -------------------------------------------------------------------------------- 1 | # This workflow warns and then closes stale issues and PRs. 2 | # Docs: https://github.com/actions/stale 3 | name: Close Stale Issues 4 | 5 | on: 6 | schedule: 7 | - cron: "17 5 * * 1-5" # run at 5:17 (UTC) on Monday to Friday 8 | workflow_dispatch: 9 | 10 | permissions: 11 | contents: read 12 | issues: write 13 | 14 | jobs: 15 | stale: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/stale@v8 19 | with: 20 | repo-token: ${{ secrets.GITHUB_TOKEN }} 21 | # Increase max operations. 22 | # When using GITHUB_TOKEN, the rate limit is 1,000 requests per hour per repository. 23 | operations-per-run: 500 24 | # Handle stale issues 25 | stale-issue-label: 'stale' 26 | # Exemptions 27 | exempt-all-issue-assignees: true 28 | exempt-issue-labels: 'support,dependencies,pinned,security' 29 | # Mark as stale 30 | days-before-issue-stale: 63 # 2 months / 9 weeks 31 | stale-issue-message: | 32 | This issue has been automatically marked as inactive because it has not had activity in the past two months. 33 | 34 | If no further activity occurs, this issue will be automatically closed in one week in order to increase our focus on active topics. 35 | # Close 36 | days-before-issue-close: 7 # 1 week 37 | close-issue-message: | 38 | This issue has been automatically closed because it has not had recent activity. Thank you for your contributions. 39 | 40 | If the issue has not been resolved, you can [find more information in our Wiki](https://wiki.safing.io/) or [continue the conversation on our Discord](https://discord.gg/safing). 41 | # TODO: Handle stale PRs 42 | days-before-pr-stale: 36500 # 100 years - effectively disabled. 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | cpu.out 2 | vendor 3 | cmd/jess* 4 | dist 5 | 6 | # Custom dev deps 7 | go.mod.* 8 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # Docs: 2 | # https://golangci-lint.run/usage/linters/ 3 | 4 | linters: 5 | enable-all: true 6 | disable: 7 | - containedctx 8 | - contextcheck 9 | - cyclop 10 | - depguard 11 | - exhaustivestruct 12 | - exhaustruct 13 | - forbidigo 14 | - funlen 15 | - gochecknoglobals 16 | - gochecknoinits 17 | - gocognit 18 | - gocyclo 19 | - goerr113 20 | - gomnd 21 | - ifshort 22 | - interfacebloat 23 | - interfacer 24 | - ireturn 25 | - lll 26 | - musttag 27 | - nestif 28 | - nilnil 29 | - nlreturn 30 | - noctx 31 | - nolintlint 32 | - nonamedreturns 33 | - nosnakecase 34 | - revive 35 | - tagliatelle 36 | - testpackage 37 | - varnamelen 38 | - whitespace 39 | - wrapcheck 40 | - wsl 41 | 42 | linters-settings: 43 | revive: 44 | # See https://github.com/mgechev/revive#available-rules for details. 45 | enable-all-rules: true 46 | gci: 47 | # put imports beginning with prefix after 3rd-party packages; 48 | # only support one prefix 49 | # if not set, use goimports.local-prefixes 50 | local-prefixes: github.com/safing 51 | godox: 52 | # report any comments starting with keywords, this is useful for TODO or FIXME comments that 53 | # might be left in the code accidentally and should be resolved before merging 54 | keywords: 55 | - FIXME 56 | gosec: 57 | # To specify a set of rules to explicitly exclude. 58 | # Available rules: https://github.com/securego/gosec#available-rules 59 | excludes: 60 | - G204 # Variables in commands. 61 | - G304 # Variables in file paths. 62 | - G505 # We need crypto/sha1 for non-security stuff. Using `nolint:` triggers another linter. 63 | 64 | issues: 65 | exclude-use-default: false 66 | exclude-rules: 67 | - text: "a blank import .*" 68 | linters: 69 | - golint 70 | - text: "ST1000: at least one file in a package should have a package comment.*" 71 | linters: 72 | - stylecheck 73 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.x 5 | 6 | os: 7 | - linux 8 | - windows 9 | - osx 10 | 11 | branches: 12 | only: 13 | - master 14 | - develop 15 | - /^feature\/travis\/.+$/ # feature/travis/* 16 | - /^fix\/travis\/.+$/ # fix/travis/* 17 | - /^v.*$/ # version tags 18 | 19 | git: 20 | autocrlf: false # gofmt doesn't like CRLF 21 | 22 | install: 23 | - go get -d -u github.com/golang/dep 24 | - go install github.com/golang/dep/cmd/dep 25 | - dep ensure 26 | - ./test install 27 | 28 | script: 29 | - ./test --scripted 30 | - ./pack build-os 31 | 32 | deploy: 33 | provider: releases 34 | token: 35 | secure: "Qj3iEGWiAH7uTfOcY6Hi1qF573R5eKjoiJRKgbkt8W7JNOeW+QJD/Vv78q3tpY3UkG1Ez4sOWRsXHrCF6V462NFoY/VFsb5V1i8WP9+v0Z0uNtYFWfWcp0HBN7jT9xsbCwnF4KnaWx+7hOpxeY+L6bBDnsIXMnK/rOWI+HdM2IFdXSEqvpoBERGyNKuPJMdssvX2tbitvRmj13RVZWQoBvxUr2DB8WAavG4afuqwkzoIHw11HpRf2v8BZ8eB1rO6FxaaC2yb8GsFwKsKLUVuqS5carZQVewHSAifh4Zq3f6fZDYRR5gBm8pLeMghWIt6rwo8L1/Fn3uZUkhKFLUR3zrEkxoHdf4jZjJ1oC4zcSDHJKA20QVCTfZGM1OaXmS7UzftRz/855tGvF746M1gSNzMPNmK2thgEgxW3UlOxbSSMvd5NDpTyPYn+DAW3lPDRNNH9a0t+C1mfb3SI4uHl+QaQ9BKSLpIwOJRVEAbrl7Vt7gs5pLJmj3bcwiZ3jjfEwuTNg6n+5QypUdWDY3sQ0EQVOHOHuSRR2TcnSd8wvVPKY7LZ+Fq8Dm0/lTKnz9pyy1psdUZpTEZ97IO3y7gFg3GSKGOoKkx94V5QtTSM9h3TFGFAF275n0MO5LTKyWZtT/1x9/G1k80fNAOHE9cooJAw580uI305pr3r3hjmN0=" 36 | file_glob: true 37 | file: dist/* 38 | skip_cleanup: true 39 | overwrite: true 40 | on: 41 | tags: true 42 | 43 | # encrypting the Github Peronal Access Token: 44 | # docker run --rm -ti ruby /bin/bash 45 | # gem install travis 46 | # travis encrypt -r safing/jess 47 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | All files in this repository (unless otherwise noted) are authored, owned and copyrighted by Safing ICS Technologies GmbH (Austria). 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at coc@safing.io. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /cmd/README.md: -------------------------------------------------------------------------------- 1 | ### Jess CLI 2 | 3 | This is currently still more of a planning and working document. 4 | Here is the CLI interface that is planned: 5 | 6 | ``` 7 | jess create 8 | 9 | jess close with 10 | encrypt a file, write to file with the same name, but with a .letter suffix 11 | -o ... write output to 12 | 13 | jess open 14 | decrypt a file, write to file with the same name, but without the .letter suffix 15 | -o ... write output to 16 | 17 | jess sign with 18 | same as close, but will put the signature in a separate file called .seal 19 | 20 | jess verify 21 | verifies the signature(s), but does not decrypt 22 | 23 | jess show 24 | shows all available information about said file. File can be 25 | - envelope 26 | - letter 27 | - seal (signature-only letter) 28 | 29 | jess generate 30 | generate a new signet and store both signet and recipient in the truststore 31 | 32 | global arguments 33 | --tsdir /path/to/truststore 34 | --seclevel 35 | --symkeysize 36 | --quiet only output errors and warnings 37 | ``` 38 | -------------------------------------------------------------------------------- /cmd/build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eo pipefail 3 | 4 | baseDir="$( cd "$(dirname "$0")" && pwd )" 5 | cd "$baseDir" 6 | 7 | echo "Please notice, that this build script includes metadata into the build." 8 | echo "This information is useful for debugging and license compliance." 9 | echo "Run the compiled binary with the version command to see the information included." 10 | 11 | # Get version. 12 | VERSION="$(git tag --points-at)" || true 13 | test -z "$VERSION" && DEV_VERSION="$(git describe --tags --first-parent --abbrev=0)" || true 14 | test -n "$DEV_VERSION" && VERSION="${DEV_VERSION}_dev_build" 15 | test -z "$VERSION" && VERSION="dev_build" 16 | BUILD_SOURCE=$( ( git remote -v | cut -f2 | cut -d" " -f1 | head -n 1 ) || echo "unknown" ) 17 | BUILD_TIME=$(date -u "+%Y-%m-%dT%H:%M:%SZ" || echo "unknown") 18 | 19 | LDFLAGS="-X main.Version=${VERSION} -X main.BuildSource=${BUILD_SOURCE} -X main.BuildTime=${BUILD_TIME}" 20 | 21 | # build output name 22 | BIN_NAME="jess" 23 | if [[ "$GOOS" == "windows" ]]; then 24 | BIN_NAME="${BIN_NAME}.exe" 25 | fi 26 | 27 | # Build. 28 | export CGO_ENABLED=0 29 | go build -o "${BIN_NAME}" -ldflags "$LDFLAGS" "$@" 30 | -------------------------------------------------------------------------------- /cmd/cfg-tools.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/AlecAivazis/survey/v2" 8 | 9 | "github.com/safing/jess/hashtools" 10 | "github.com/safing/jess/tools" 11 | ) 12 | 13 | func pickTools(toolNames []string, promptMsg string) ([]string, error) { //nolint:unused,deadcode // TODO 14 | var toolSelection [][]string //nolint:prealloc 15 | preSelectedTools := make([]string, 0, len(toolNames)) 16 | var preSelected int 17 | 18 | // place already configured tools at top 19 | for _, toolName := range toolNames { 20 | toolID := toolName 21 | if strings.Contains(toolID, "(") { 22 | toolID = strings.Split(toolID, "(")[0] 23 | } 24 | 25 | tool, err := tools.Get(toolID) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | toolSelection = append(toolSelection, []string{ 31 | toolName, 32 | tool.Info.FormatPurpose(), 33 | formatToolSecurityLevel(tool), 34 | tool.Info.Author, 35 | tool.Info.Comment, 36 | }) 37 | preSelectedTools = append(preSelectedTools, tool.Info.Name) 38 | 39 | preSelected++ 40 | } 41 | 42 | // add all other tools 43 | for _, tool := range tools.AsList() { 44 | if stringInSlice(tool.Info.Name, preSelectedTools) { 45 | continue 46 | } 47 | 48 | toolSelection = append(toolSelection, []string{ 49 | tool.Info.Name, 50 | tool.Info.FormatPurpose(), 51 | formatToolSecurityLevel(tool), 52 | tool.Info.Author, 53 | tool.Info.Comment, 54 | }) 55 | } 56 | 57 | // select 58 | var selectedEntries []string 59 | formattedColumns := formatColumns(toolSelection) 60 | selectTools := &survey.MultiSelect{ 61 | Message: promptMsg, 62 | Options: formattedColumns, 63 | Default: formattedColumns[:preSelected], 64 | PageSize: 15, 65 | } 66 | err := survey.AskOne(selectTools, &selectedEntries, nil) 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | // check selection 72 | newTools := make([]string, 0, len(selectedEntries)) 73 | for _, entry := range selectedEntries { 74 | toolName := strings.Fields(entry)[0] 75 | if strings.Contains(toolName, "(") { 76 | newTools = append(newTools, toolName) 77 | continue 78 | } 79 | 80 | // get tool 81 | tool, err := tools.Get(toolName) 82 | if err != nil { 83 | return nil, err 84 | } 85 | 86 | // check if tool needs hasher 87 | if tool.Info.HasOption(tools.OptionNeedsDedicatedHasher) || 88 | tool.Info.HasOption(tools.OptionNeedsManagedHasher) { 89 | // add hash tool 90 | hashToolName, err := pickHashTool(fmt.Sprintf("Select hash tool for %s:", toolName), tool.Info.SecurityLevel) 91 | if err != nil { 92 | return nil, err 93 | } 94 | newTools = append(newTools, fmt.Sprintf("%s(%s)", toolName, hashToolName)) 95 | } else { 96 | newTools = append(newTools, toolName) 97 | } 98 | } 99 | 100 | return newTools, nil 101 | } 102 | 103 | func pickHashTool(prompt string, minSecurityLevel int) (string, error) { //nolint:unused // TODO 104 | var hashToolSelection [][]string 105 | for _, hashTool := range hashtools.AsList() { 106 | if hashTool.SecurityLevel >= minSecurityLevel { 107 | hashToolSelection = append(hashToolSelection, []string{ 108 | hashTool.Name, 109 | fmt.Sprintf("%d b/s", hashTool.SecurityLevel), 110 | hashTool.Author, 111 | hashTool.Comment, 112 | }) 113 | } 114 | } 115 | var selectedEnty string 116 | selectHashTool := &survey.Select{ 117 | Message: prompt, 118 | Options: formatColumns(hashToolSelection), 119 | PageSize: 15, 120 | } 121 | err := survey.AskOne(selectHashTool, &selectedEnty, nil) 122 | if err != nil { 123 | return "", err 124 | } 125 | return strings.Fields(selectedEnty)[0], nil 126 | } 127 | 128 | func stringInSlice(s string, a []string) bool { //nolint:unused // TODO 129 | for _, entry := range a { 130 | if entry == s { 131 | return true 132 | } 133 | } 134 | return false 135 | } 136 | -------------------------------------------------------------------------------- /cmd/cmd-checksum.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/spf13/cobra" 10 | 11 | "github.com/safing/jess/filesig" 12 | ) 13 | 14 | func init() { 15 | rootCmd.AddCommand(checksum) 16 | checksum.AddCommand(checksumAdd) 17 | checksum.AddCommand(checksumVerify) 18 | } 19 | 20 | var ( 21 | checksum = &cobra.Command{ 22 | Use: "checksum", 23 | Short: "add or verify embedded checksums", 24 | } 25 | 26 | checksumAddUsage = "usage: checksum add " 27 | checksumAdd = &cobra.Command{ 28 | Use: "add ", 29 | Short: "add an embedded checksum to a file", 30 | Long: "add an embedded checksum to a file (support file types: txt, json, yaml)", 31 | RunE: handleChecksumAdd, 32 | } 33 | 34 | checksumVerifyUsage = "usage: checksum verify " 35 | checksumVerify = &cobra.Command{ 36 | Use: "verify ", 37 | Short: "verify the embedded checksum of a file", 38 | Long: "verify the embedded checksum of a file (support file types: txt, json, yaml)", 39 | RunE: handleChecksumVerify, 40 | } 41 | ) 42 | 43 | func handleChecksumAdd(cmd *cobra.Command, args []string) error { 44 | // Check args. 45 | if len(args) != 1 { 46 | return errors.New(checksumAddUsage) 47 | } 48 | filename := args[0] 49 | 50 | data, err := os.ReadFile(filename) 51 | if err != nil { 52 | return fmt.Errorf("failed to read file: %w", err) 53 | } 54 | 55 | switch filepath.Ext(filename) { 56 | case ".json": 57 | data, err = filesig.AddJSONChecksum(data) 58 | case ".yml", ".yaml": 59 | data, err = filesig.AddYAMLChecksum(data, filesig.TextPlacementAfterComment) 60 | case ".txt": 61 | data, err = filesig.AddTextFileChecksum(data, "#", filesig.TextPlacementAfterComment) 62 | default: 63 | return errors.New("unsupported file format") 64 | } 65 | if err != nil { 66 | return err 67 | } 68 | 69 | // Write back to disk. 70 | fileInfo, err := os.Stat(filename) 71 | if err != nil { 72 | return fmt.Errorf("failed to stat file: %w", err) 73 | } 74 | err = os.WriteFile(filename, data, fileInfo.Mode().Perm()) 75 | if err != nil { 76 | return fmt.Errorf("failed to write back file with checksum: %w", err) 77 | } 78 | 79 | fmt.Println("checksum added") 80 | return nil 81 | } 82 | 83 | func handleChecksumVerify(cmd *cobra.Command, args []string) error { 84 | // Check args. 85 | if len(args) != 1 { 86 | return errors.New(checksumVerifyUsage) 87 | } 88 | filename := args[0] 89 | 90 | data, err := os.ReadFile(filename) 91 | if err != nil { 92 | return fmt.Errorf("failed to read file: %w", err) 93 | } 94 | 95 | switch filepath.Ext(filename) { 96 | case ".json": 97 | err = filesig.VerifyJSONChecksum(data) 98 | case ".yml", ".yaml": 99 | err = filesig.VerifyYAMLChecksum(data) 100 | case ".txt": 101 | err = filesig.VerifyTextFileChecksum(data, "#") 102 | default: 103 | return errors.New("unsupported file format") 104 | } 105 | if err != nil { 106 | return err 107 | } 108 | 109 | fmt.Println("checksum verified") 110 | return nil 111 | } 112 | -------------------------------------------------------------------------------- /cmd/cmd-close.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "io/fs" 8 | "os" 9 | "strings" 10 | 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | func init() { 15 | rootCmd.AddCommand(closeCmd) 16 | closeCmd.Flags().StringVarP(&closeFlagOutput, "output", "o", "", "specify output file (`-` for stdout)") 17 | } 18 | 19 | var ( 20 | closeFlagOutput string 21 | closeCmdHelp = "usage: jess close with " 22 | 23 | closeCmd = &cobra.Command{ 24 | Use: "close with ", 25 | Short: "encrypt file", 26 | Long: "encrypt file with the given envelope. Use `-` to use stdin", 27 | DisableFlagsInUseLine: true, 28 | PreRunE: requireTrustStore, 29 | RunE: func(cmd *cobra.Command, args []string) error { 30 | registerPasswordCallbacks() 31 | 32 | // check args 33 | if len(args) != 3 || args[1] != "with" { 34 | return errors.New(closeCmdHelp) 35 | } 36 | 37 | // get envelope 38 | envelope, err := trustStore.GetEnvelope(args[2]) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | // create session (check envelope) 44 | session, err := envelope.Correspondence(trustStore) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | // check filenames 50 | filename := args[0] 51 | outputFilename := closeFlagOutput 52 | if outputFilename == "" { 53 | if strings.HasSuffix(filename, letterFileExtension) { 54 | return errors.New("cannot automatically derive output filename, please specify with --output") 55 | } 56 | outputFilename = filename + letterFileExtension 57 | } 58 | // check input file 59 | if filename != "-" { 60 | fileInfo, err := os.Stat(filename) 61 | if err != nil { 62 | return err 63 | } 64 | if fileInfo.Size() > warnFileSize { 65 | confirmed, err := confirm("Input file is really big (%s) and jess needs to load it fully to memory, continue?", true) 66 | if err != nil { 67 | return err 68 | } 69 | if !confirmed { 70 | return nil 71 | } 72 | } 73 | } 74 | // check output file 75 | if outputFilename != "-" { 76 | _, err = os.Stat(outputFilename) 77 | if err == nil { 78 | confirmed, err := confirm("Output file already exists, overwrite?", true) 79 | if err != nil { 80 | return err 81 | } 82 | if !confirmed { 83 | return nil 84 | } 85 | } else if !errors.Is(err, fs.ErrNotExist) { 86 | return fmt.Errorf("failed to access output file: %w", err) 87 | } 88 | } 89 | 90 | // load file 91 | var data []byte 92 | if filename == "-" { 93 | data, err = io.ReadAll(os.Stdin) 94 | } else { 95 | data, err = os.ReadFile(filename) 96 | } 97 | if err != nil { 98 | return err 99 | } 100 | 101 | // encrypt 102 | letter, err := session.Close(data) 103 | if err != nil { 104 | return err 105 | } 106 | 107 | // to file format 108 | c, err := letter.ToFileFormat() 109 | if err != nil { 110 | return err 111 | } 112 | 113 | // open file for writing 114 | var file *os.File 115 | if outputFilename == "-" { 116 | file = os.Stdout 117 | } else { 118 | file, err = os.OpenFile( 119 | outputFilename, 120 | os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 121 | 0o0600, 122 | ) 123 | if err != nil { 124 | return err 125 | } 126 | } 127 | 128 | // write 129 | err = c.WriteAllTo(file) 130 | if err != nil { 131 | _ = file.Close() 132 | return err 133 | } 134 | return file.Close() 135 | }, 136 | } 137 | ) 138 | -------------------------------------------------------------------------------- /cmd/cmd-configure.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/safing/jess" 9 | "github.com/safing/jess/truststores" 10 | ) 11 | 12 | func init() { 13 | rootCmd.AddCommand(configureCmd) 14 | } 15 | 16 | var configureCmd = &cobra.Command{ 17 | Use: "configure ", 18 | Short: "configure (and create) envelope", 19 | DisableFlagsInUseLine: true, 20 | Args: cobra.MaximumNArgs(1), 21 | PreRunE: requireTrustStore, 22 | RunE: func(cmd *cobra.Command, args []string) (err error) { 23 | // check envelope name existence 24 | if len(args) == 0 { 25 | return errors.New("please specify an envelope name") 26 | } 27 | envelopeName := args[0] 28 | 29 | // check envelope name 30 | if !truststores.NamePlaysNiceWithFS(envelopeName) { 31 | return errors.New("please only use alphanumeric characters and `- ._+@` for best compatibility with various systems") 32 | } 33 | 34 | // get envelope from trust store 35 | envelope, err := trustStore.GetEnvelope(envelopeName) 36 | if err != nil && !errors.Is(err, jess.ErrEnvelopeNotFound) { 37 | return err 38 | } 39 | 40 | // create 41 | if envelope == nil { 42 | envelope, err = newEnvelope(envelopeName) 43 | if err != nil { 44 | return err 45 | } 46 | } 47 | 48 | // edit (and save) 49 | return editEnvelope(envelope) 50 | }, 51 | } 52 | -------------------------------------------------------------------------------- /cmd/cmd-generate.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func init() { 10 | rootCmd.AddCommand(generateCmd) 11 | generateCmd.Flags().StringVarP(&generateFlagName, "name", "l", "", "specify signet name/label") 12 | generateCmd.Flags().StringVarP(&generateFlagScheme, "scheme", "t", "", "specify signet scheme/tool") 13 | generateCmd.Flags().BoolVarP(&generateFlagTextOnly, "textonly", "", false, "do not save to trust store and only output directly as text") 14 | } 15 | 16 | var ( 17 | generateFlagName string 18 | generateFlagScheme string 19 | generateFlagTextOnly bool 20 | 21 | generateCmd = &cobra.Command{ 22 | Use: "generate", 23 | Short: "generate a new signet", 24 | DisableFlagsInUseLine: true, 25 | Args: cobra.NoArgs, 26 | PreRunE: requireTrustStore, 27 | RunE: func(cmd *cobra.Command, args []string) error { 28 | // Generate new signet 29 | signet, err := newSignet(generateFlagName, generateFlagScheme, !generateFlagTextOnly) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | // Output as text if not saved to trust store. 35 | if generateFlagTextOnly { 36 | // Make text backup. 37 | backup, err := signet.Backup(false) 38 | if err != nil { 39 | return err 40 | } 41 | 42 | // Convert to recipient and serialize key. 43 | rcpt, err := signet.AsRecipient() 44 | if err != nil { 45 | return err 46 | } 47 | err = rcpt.StoreKey() 48 | if err != nil { 49 | return err 50 | } 51 | 52 | // Make text export. 53 | export, err := rcpt.Export(false) 54 | if err != nil { 55 | return err 56 | } 57 | 58 | // Write output. 59 | fmt.Printf("Generated %s key with ID %s and name %q\n", signet.Scheme, signet.ID, signet.Info.Name) 60 | fmt.Printf("Backup (private key): %s\n", backup) 61 | fmt.Printf("Export (public key): %s\n", export) 62 | } 63 | 64 | return nil 65 | }, 66 | } 67 | ) 68 | -------------------------------------------------------------------------------- /cmd/cmd-import-export.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/safing/jess" 11 | ) 12 | 13 | func init() { 14 | rootCmd.AddCommand(exportCmd) 15 | rootCmd.AddCommand(backupCmd) 16 | rootCmd.AddCommand(importCmd) 17 | } 18 | 19 | var ( 20 | exportCmdHelp = "usage: export " 21 | exportCmd = &cobra.Command{ 22 | Use: "export ", 23 | Short: "export a signet or envelope", 24 | Long: "export a signet (as a recipient - the public key only) or an envelope (configuration)", 25 | RunE: handleExport, 26 | } 27 | 28 | backupCmdHelp = "usage: backup ", 31 | Short: "backup a signet", 32 | Long: "backup a signet (the private key - do not share!)", 33 | RunE: handleBackup, 34 | } 35 | 36 | importCmdHelp = "usage: import " 37 | importCmd = &cobra.Command{ 38 | Use: "import ", 39 | Short: "import a signet or an enveleope", 40 | Long: "import a signet (any kind) or an enveleope", 41 | RunE: handleImport, 42 | } 43 | ) 44 | 45 | func handleExport(cmd *cobra.Command, args []string) error { 46 | // Check args. 47 | if len(args) != 1 { 48 | return errors.New(exportCmdHelp) 49 | } 50 | id := args[0] 51 | 52 | // Get Recipient. 53 | recipient, err := trustStore.GetSignet(id, true) 54 | if err == nil { 55 | text, err := recipient.Export(false) 56 | if err != nil { 57 | return fmt.Errorf("failed to export recipient %s: %w", id, err) 58 | } 59 | fmt.Println(text) 60 | return nil 61 | } 62 | 63 | // Check if there is a signet instead. 64 | signet, err := trustStore.GetSignet(id, false) 65 | if err == nil { 66 | recipient, err := signet.AsRecipient() 67 | if err != nil { 68 | return fmt.Errorf("failed convert signet %s to recipient for export: %w", id, err) 69 | } 70 | text, err := recipient.Export(false) 71 | if err != nil { 72 | return fmt.Errorf("failed to export recipient %s: %w", id, err) 73 | } 74 | fmt.Println(text) 75 | return nil 76 | } 77 | 78 | // Check for an envelope. 79 | env, err := trustStore.GetEnvelope(id) 80 | if err == nil { 81 | text, err := env.Export(false) 82 | if err != nil { 83 | return fmt.Errorf("failed to export envelope %s: %w", id, err) 84 | } 85 | fmt.Println(text) 86 | return nil 87 | } 88 | 89 | return errors.New("no recipient or envelope found with the given ID") 90 | } 91 | 92 | func handleBackup(cmd *cobra.Command, args []string) error { 93 | // Check args. 94 | if len(args) != 1 { 95 | return errors.New(backupCmdHelp) 96 | } 97 | id := args[0] 98 | 99 | // Check if there is a signet instead. 100 | signet, err := trustStore.GetSignet(id, false) 101 | if err != nil { 102 | text, err := signet.Backup(false) 103 | if err != nil { 104 | return fmt.Errorf("failed to backup signet %s: %w", id, err) 105 | } 106 | fmt.Println(text) 107 | return nil 108 | } 109 | 110 | return errors.New("no signet found with the given ID") 111 | } 112 | 113 | func handleImport(cmd *cobra.Command, args []string) error { 114 | // Check args. 115 | if len(args) != 1 { 116 | return errors.New(importCmdHelp) 117 | } 118 | text := args[0] 119 | 120 | // First, check if it's an envelope. 121 | if strings.HasPrefix(text, jess.ExportEnvelopePrefix) { 122 | env, err := jess.EnvelopeFromTextFormat(text) 123 | if err != nil { 124 | return fmt.Errorf("failed to parse envelope: %w", err) 125 | } 126 | err = trustStore.StoreEnvelope(env) 127 | if err != nil { 128 | return fmt.Errorf("failed to import envelope into trust store: %w", err) 129 | } 130 | fmt.Printf("imported envelope %q intro trust store\n", env.Name) 131 | return nil 132 | } 133 | 134 | // Then handle all signet types together. 135 | var ( 136 | signetType string 137 | parseFunc func(textFormat string) (*jess.Signet, error) 138 | ) 139 | switch { 140 | case strings.HasPrefix(text, jess.ExportSenderPrefix): 141 | signetType = jess.ExportSenderKeyword 142 | parseFunc = jess.SenderFromTextFormat 143 | case strings.HasPrefix(text, jess.ExportRecipientPrefix): 144 | signetType = jess.ExportRecipientKeyword 145 | parseFunc = jess.RecipientFromTextFormat 146 | case strings.HasPrefix(text, jess.ExportKeyPrefix): 147 | signetType = jess.ExportKeyKeyword 148 | parseFunc = jess.KeyFromTextFormat 149 | default: 150 | return fmt.Errorf( 151 | "invalid format or unknown type, expected one of %s, %s, %s, %s", 152 | jess.ExportKeyKeyword, 153 | jess.ExportSenderKeyword, 154 | jess.ExportRecipientKeyword, 155 | jess.ExportEnvelopeKeyword, 156 | ) 157 | } 158 | // Parse and import 159 | signet, err := parseFunc(text) 160 | if err != nil { 161 | return fmt.Errorf("failed to parse %s: %w", signetType, err) 162 | } 163 | err = trustStore.StoreSignet(signet) 164 | if err != nil { 165 | return fmt.Errorf("failed to import %s into trust store: %w", signetType, err) 166 | } 167 | fmt.Printf("imported %s %s intro trust store\n", signetType, signet.ID) 168 | 169 | return nil 170 | } 171 | -------------------------------------------------------------------------------- /cmd/cmd-list.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/spf13/cobra" 8 | 9 | "github.com/safing/jess" 10 | "github.com/safing/jess/hashtools" 11 | "github.com/safing/jess/tools" 12 | ) 13 | 14 | func init() { 15 | rootCmd.AddCommand(listCmd) 16 | } 17 | 18 | var listCmd = &cobra.Command{ 19 | Use: "list", 20 | Short: "list all available suites and tools", 21 | DisableFlagsInUseLine: true, 22 | Run: func(cmd *cobra.Command, args []string) { 23 | fmt.Printf("Suites\n\n") 24 | suitesTable := [][]string{ 25 | {"Name/ID", "Provides", "Security Level", "Tools", "Notes"}, 26 | } 27 | for _, suite := range jess.Suites() { 28 | suitesTable = append(suitesTable, []string{ 29 | suite.ID, 30 | suite.Provides.ShortString(), 31 | formatSecurityLevel(suite.SecurityLevel), 32 | strings.Join(suite.Tools, ", "), 33 | formatSuiteStatus(suite), 34 | }) 35 | } 36 | for _, line := range formatColumns(suitesTable) { 37 | fmt.Println(line) 38 | } 39 | 40 | fmt.Printf("\n\nTools\n\n") 41 | toolTable := [][]string{ 42 | {"Name/ID", "Purpose", "Security Level", "Author", "Comment"}, 43 | } 44 | for _, tool := range tools.AsList() { 45 | toolTable = append(toolTable, []string{ 46 | tool.Info.Name, 47 | tool.Info.FormatPurpose(), 48 | formatToolSecurityLevel(tool), 49 | tool.Info.Author, 50 | tool.Info.Comment, 51 | }) 52 | } 53 | for _, line := range formatColumns(toolTable) { 54 | fmt.Println(line) 55 | } 56 | 57 | fmt.Printf("\n\nHashTools\n\n") 58 | hashToolTable := [][]string{ 59 | {"Name/ID", "Security Level", "Author", "Comment"}, 60 | } 61 | for _, hashTool := range hashtools.AsList() { 62 | hashToolTable = append(hashToolTable, []string{ 63 | hashTool.Name, 64 | fmt.Sprintf("%d b/s", hashTool.SecurityLevel), 65 | hashTool.Author, 66 | hashTool.Comment, 67 | }) 68 | } 69 | for _, line := range formatColumns(hashToolTable) { 70 | fmt.Println(line) 71 | } 72 | }, 73 | } 74 | -------------------------------------------------------------------------------- /cmd/cmd-manage.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/AlecAivazis/survey/v2" 9 | "github.com/spf13/cobra" 10 | 11 | "github.com/safing/jess" 12 | ) 13 | 14 | const ( 15 | failPlaceholder = "[fail]" 16 | ) 17 | 18 | func init() { 19 | rootCmd.AddCommand(manageCmd) 20 | } 21 | 22 | var manageCmd = &cobra.Command{ 23 | Use: "manage", 24 | Short: "manage a trust store", 25 | DisableFlagsInUseLine: true, 26 | Args: cobra.MaximumNArgs(1), 27 | PreRunE: requireTrustStore, 28 | RunE: func(cmd *cobra.Command, args []string) error { 29 | // select action 30 | var selectedAction string 31 | selectAction := &survey.Select{ 32 | Message: "Manage:", 33 | Options: []string{ 34 | "Envelopes", 35 | "Signets", 36 | }, 37 | PageSize: 15, 38 | } 39 | err := survey.AskOne(selectAction, &selectedAction, nil) 40 | if err != nil { 41 | return err 42 | } 43 | 44 | switch selectedAction { 45 | case "Envelopes": 46 | return manageEnvelopes() 47 | case "Signets": 48 | return manageSignets() 49 | default: 50 | fmt.Println("internal error") 51 | } 52 | return nil 53 | }, 54 | } 55 | 56 | func manageSignets() error { 57 | for { 58 | // get signets 59 | all, err := trustStore.SelectSignets(jess.FilterAny) 60 | if err != nil { 61 | return err 62 | } 63 | 64 | // select signet 65 | signets, err := pickSignet(all, "Select to manage:", "Done", false, nil) 66 | if err != nil { 67 | return err 68 | } 69 | switch len(signets) { 70 | case 0: 71 | return nil // selected done msg 72 | case 1: 73 | // valid 74 | default: 75 | return errors.New("internal error: failed to select signet") 76 | } 77 | selectedSignet := signets[0] 78 | 79 | // select action 80 | var selectedAction string 81 | selectAction := &survey.Select{ 82 | Message: "Select action:", 83 | Options: []string{ 84 | "Delete", 85 | "Back to list", 86 | }, 87 | PageSize: 15, 88 | } 89 | err = survey.AskOne(selectAction, &selectedAction, nil) 90 | if err != nil { 91 | return err 92 | } 93 | 94 | switch selectedAction { 95 | case "Delete": 96 | err = trustStore.DeleteSignet(selectedSignet.ID, selectedSignet.Public) 97 | if err != nil { 98 | return err 99 | } 100 | case "Back to list": 101 | continue 102 | default: 103 | fmt.Println("internal error") 104 | } 105 | } 106 | } 107 | 108 | func manageEnvelopes() error { 109 | for { 110 | // get envelopes 111 | all, err := trustStore.AllEnvelopes() 112 | if err != nil { 113 | return err 114 | } 115 | 116 | selection := [][]string{ 117 | {"Done"}, 118 | } 119 | for _, envelope := range all { 120 | selection = append(selection, []string{ 121 | envelope.Name, 122 | envelope.SuiteID, 123 | fmt.Sprintf("provides %s and %s", 124 | envelope.Suite().Provides.ShortString(), 125 | formatSecurityLevel(envelope.Suite().SecurityLevel), 126 | ), 127 | formatEnvelopeSignets(envelope), 128 | }) 129 | } 130 | 131 | var selectedEnvelopeEntry string 132 | selectEnvelope := &survey.Select{ 133 | Message: "Select to manage:", 134 | Options: formatColumns(selection), 135 | PageSize: 15, 136 | } 137 | err = survey.AskOne(selectEnvelope, &selectedEnvelopeEntry, nil) 138 | if err != nil { 139 | return err 140 | } 141 | 142 | if strings.HasPrefix(selectedEnvelopeEntry, "Done") { 143 | return nil 144 | } 145 | 146 | selectedEnvelopeName := strings.Fields(selectedEnvelopeEntry)[0] 147 | for _, envelope := range all { 148 | if envelope.Name == selectedEnvelopeName { 149 | err := editEnvelope(envelope) 150 | if err != nil { 151 | return err 152 | } 153 | break 154 | } 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /cmd/cmd-open.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "io/fs" 8 | "os" 9 | "strings" 10 | 11 | "github.com/spf13/cobra" 12 | 13 | "github.com/safing/jess" 14 | "github.com/safing/structures/container" 15 | ) 16 | 17 | func init() { 18 | rootCmd.AddCommand(openCmd) 19 | openCmd.Flags().StringVarP(&openFlagOutput, "output", "o", "", "specify output file (`-` for stdout") 20 | } 21 | 22 | var ( 23 | openFlagOutput string 24 | openCmdHelp = "usage: jess open " 25 | 26 | openCmd = &cobra.Command{ 27 | Use: "open ", 28 | Short: "decrypt file", 29 | Long: "decrypt file with the given envelope. Use `-` to use stdin", 30 | RunE: func(cmd *cobra.Command, args []string) (err error) { 31 | registerPasswordCallbacks() 32 | 33 | // check args 34 | if len(args) != 1 { 35 | return errors.New(openCmdHelp) 36 | } 37 | 38 | // check filenames 39 | filename := args[0] 40 | outputFilename := openFlagOutput 41 | if outputFilename == "" { 42 | if !strings.HasSuffix(filename, ".letter") || len(filename) < 8 { 43 | return errors.New("cannot automatically derive output filename, please specify with --output") 44 | } 45 | outputFilename = strings.TrimSuffix(filename, ".letter") 46 | } 47 | // check input file 48 | if filename != "-" { 49 | fileInfo, err := os.Stat(filename) 50 | if err != nil { 51 | return err 52 | } 53 | if fileInfo.Size() > warnFileSize { 54 | confirmed, err := confirm("Input file is really big (%s) and jess needs to load it fully to memory, continue?", true) 55 | if err != nil { 56 | return err 57 | } 58 | if !confirmed { 59 | return nil 60 | } 61 | } 62 | } 63 | // check output file 64 | if outputFilename != "-" { 65 | _, err = os.Stat(outputFilename) 66 | if err == nil { 67 | confirmed, err := confirm("Output file already exists, overwrite?", true) 68 | if err != nil { 69 | return err 70 | } 71 | if !confirmed { 72 | return nil 73 | } 74 | } else if !errors.Is(err, fs.ErrNotExist) { 75 | return fmt.Errorf("failed to access output file: %w", err) 76 | } 77 | } 78 | 79 | // load file 80 | var data []byte 81 | if filename == "-" { 82 | data, err = io.ReadAll(os.Stdin) 83 | } else { 84 | data, err = os.ReadFile(filename) 85 | } 86 | if err != nil { 87 | return err 88 | } 89 | 90 | // parse file 91 | letter, err := jess.LetterFromFileFormat(container.New(data)) 92 | if err != nil { 93 | return err 94 | } 95 | 96 | // Create default requirements if not set. 97 | if requirements == nil { 98 | requirements = jess.NewRequirements() 99 | } 100 | 101 | // decrypt (and verify) 102 | plainText, err := letter.Open(requirements, trustStore) 103 | if err != nil { 104 | return err 105 | } 106 | 107 | // open file for writing 108 | var file *os.File 109 | if outputFilename == "-" { 110 | file = os.Stdout 111 | } else { 112 | file, err = os.OpenFile( 113 | outputFilename, 114 | os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 115 | 0o0600, 116 | ) 117 | if err != nil { 118 | return err 119 | } 120 | } 121 | 122 | // write 123 | n, err := file.Write(plainText) 124 | if err != nil { 125 | _ = file.Close() 126 | return err 127 | } 128 | if n < len(plainText) { 129 | _ = file.Close() 130 | return io.ErrShortWrite 131 | } 132 | return file.Close() 133 | }, 134 | } 135 | ) 136 | -------------------------------------------------------------------------------- /cmd/cmd-sign.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/safing/jess/filesig" 11 | ) 12 | 13 | func init() { 14 | rootCmd.AddCommand(signCmd) 15 | signCmd.Flags().StringVarP(&closeFlagOutput, "output", "o", "", "specify output file (`-` for stdout") 16 | signCmd.Flags().StringToStringVarP(&metaDataFlag, "metadata", "m", nil, "specify file metadata to sign") 17 | } 18 | 19 | var ( 20 | metaDataFlag map[string]string 21 | signCmdHelp = "usage: jess sign with " 22 | 23 | signCmd = &cobra.Command{ 24 | Use: "sign with ", 25 | Short: "sign file", 26 | Long: "sign file with the given envelope. Use `-` to use stdin", 27 | DisableFlagsInUseLine: true, 28 | PreRunE: requireTrustStore, 29 | RunE: func(cmd *cobra.Command, args []string) error { 30 | registerPasswordCallbacks() 31 | 32 | // check args 33 | if len(args) != 3 || args[1] != "with" { 34 | return errors.New(signCmdHelp) 35 | } 36 | 37 | // get envelope 38 | envelope, err := trustStore.GetEnvelope(args[2]) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | // check filenames 44 | filename := args[0] 45 | outputFilename := closeFlagOutput 46 | if outputFilename == "" { 47 | if strings.HasSuffix(filename, filesig.Extension) { 48 | return errors.New("cannot automatically derive output filename, please specify with --output") 49 | } 50 | outputFilename = filename + filesig.Extension 51 | } 52 | 53 | fd, err := filesig.SignFile(filename, outputFilename, metaDataFlag, envelope, trustStore) 54 | if err != nil { 55 | return err 56 | } 57 | 58 | fmt.Print(formatSignatures(filename, outputFilename, []*filesig.FileData{fd})) 59 | return nil 60 | }, 61 | } 62 | ) 63 | -------------------------------------------------------------------------------- /cmd/cmd-version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | "runtime/debug" 7 | "strings" 8 | 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | func init() { 13 | rootCmd.AddCommand(versionCmd) 14 | } 15 | 16 | var ( 17 | // Version is the version of this command. 18 | Version = "dev build" 19 | // BuildSource holds the primary source repo used to build. 20 | BuildSource = "unknown" 21 | // BuildTime holds the time when the binary was built. 22 | BuildTime = "unknown" 23 | ) 24 | 25 | func init() { 26 | // Convert version string space placeholders. 27 | Version = strings.ReplaceAll(Version, "_", " ") 28 | BuildSource = strings.ReplaceAll(BuildSource, "_", " ") 29 | BuildTime = strings.ReplaceAll(BuildTime, "_", " ") 30 | 31 | // Get build info. 32 | buildInfo, _ := debug.ReadBuildInfo() 33 | buildSettings := make(map[string]string) 34 | for _, setting := range buildInfo.Settings { 35 | buildSettings[setting.Key] = setting.Value 36 | } 37 | 38 | // Add "dev build" to version if repo is dirty. 39 | if buildSettings["vcs.modified"] == "true" && 40 | !strings.HasSuffix(Version, "dev build") { 41 | Version += " dev build" 42 | } 43 | 44 | rootCmd.AddCommand(versionCmd) 45 | } 46 | 47 | var versionCmd = &cobra.Command{ 48 | Use: "version", 49 | Run: version, 50 | } 51 | 52 | func version(cmd *cobra.Command, args []string) { 53 | builder := new(strings.Builder) 54 | 55 | // Get build info. 56 | buildInfo, _ := debug.ReadBuildInfo() 57 | buildSettings := make(map[string]string) 58 | for _, setting := range buildInfo.Settings { 59 | buildSettings[setting.Key] = setting.Value 60 | } 61 | 62 | // Print version info. 63 | builder.WriteString(fmt.Sprintf("Jess %s\n", Version)) 64 | 65 | // Build info. 66 | cgoInfo := "-cgo" 67 | if buildSettings["CGO_ENABLED"] == "1" { 68 | cgoInfo = "+cgo" 69 | } 70 | builder.WriteString(fmt.Sprintf("\nbuilt with %s (%s %s) for %s/%s\n", runtime.Version(), runtime.Compiler, cgoInfo, runtime.GOOS, runtime.GOARCH)) 71 | builder.WriteString(fmt.Sprintf(" at %s\n", BuildTime)) 72 | 73 | // Commit info. 74 | dirtyInfo := "clean" 75 | if buildSettings["vcs.modified"] == "true" { 76 | dirtyInfo = "dirty" 77 | } 78 | builder.WriteString(fmt.Sprintf("\ncommit %s (%s)\n", buildSettings["vcs.revision"], dirtyInfo)) 79 | builder.WriteString(fmt.Sprintf(" at %s\n", buildSettings["vcs.time"])) 80 | builder.WriteString(fmt.Sprintf(" from %s\n", BuildSource)) 81 | 82 | // License info. 83 | builder.WriteString("\nLicensed under the GPLv3 license.") 84 | 85 | _, _ = fmt.Println(builder.String()) 86 | } 87 | -------------------------------------------------------------------------------- /cmd/format.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "errors" 7 | "fmt" 8 | "strings" 9 | "text/tabwriter" 10 | 11 | "github.com/safing/jess" 12 | "github.com/safing/jess/tools" 13 | ) 14 | 15 | func formatColumns(table [][]string) []string { 16 | buf := bytes.NewBuffer(nil) 17 | 18 | // format table with tab writer 19 | tabWriter := tabwriter.NewWriter(buf, 8, 4, 3, ' ', 0) 20 | for i := 0; i < len(table); i++ { 21 | if i > 0 { 22 | // linebreak 23 | fmt.Fprint(tabWriter, "\n") 24 | } 25 | fmt.Fprint(tabWriter, strings.Join(table[i], "\t")) 26 | } 27 | _ = tabWriter.Flush() 28 | 29 | // parse to []string 30 | var lines []string 31 | scanner := bufio.NewScanner(buf) 32 | for scanner.Scan() { 33 | lines = append(lines, scanner.Text()) 34 | } 35 | if err := scanner.Err(); err != nil { 36 | return nil 37 | } 38 | 39 | return lines 40 | } 41 | 42 | func formatSecurityLevel(securityLevel int) string { 43 | return fmt.Sprintf("%d b/s", securityLevel) 44 | } 45 | 46 | func formatToolSecurityLevel(tool *tools.Tool) string { 47 | if tool.Info.HasOption(tools.OptionNeedsSecurityLevel) { 48 | return "dynamic b/s (set manually via --seclevel)" 49 | } 50 | if tool.Info.SecurityLevel == 0 { 51 | return "" 52 | } 53 | return formatSecurityLevel(tool.Info.SecurityLevel) 54 | } 55 | 56 | func formatSignetName(signet *jess.Signet) string { 57 | switch { 58 | case signet.Info != nil && signet.Info.Name != "": 59 | return signet.Info.Name 60 | case signet.ID != "": 61 | return signet.ID 62 | default: 63 | return "[unknown]" 64 | } 65 | } 66 | 67 | func formatSignetType(signet *jess.Signet) string { 68 | switch { 69 | case signet.Scheme == jess.SignetSchemeKey: 70 | return "key" 71 | case signet.Scheme == jess.SignetSchemePassword: 72 | return "password" 73 | case signet.Public: 74 | return "recipient" 75 | default: 76 | return "signet" 77 | } 78 | } 79 | 80 | func formatSignetScheme(signet *jess.Signet) string { 81 | switch signet.Scheme { 82 | case jess.SignetSchemeKey, jess.SignetSchemePassword: 83 | return "" 84 | default: 85 | return signet.Scheme 86 | } 87 | } 88 | 89 | func formatSignetPurpose(signet *jess.Signet) string { 90 | switch signet.Scheme { 91 | case jess.SignetSchemeKey, jess.SignetSchemePassword: 92 | return "" 93 | } 94 | 95 | tool, err := signet.Tool() 96 | if err != nil { 97 | return "[unknown]" 98 | } 99 | return tool.Info.FormatPurpose() 100 | } 101 | 102 | func formatSignetSecurityLevel(signet *jess.Signet) string { 103 | switch signet.Scheme { 104 | case jess.SignetSchemeKey, jess.SignetSchemePassword: 105 | return "" 106 | } 107 | 108 | tool, err := signet.Tool() 109 | if err != nil { 110 | return failPlaceholder 111 | } 112 | 113 | securityLevel, err := tool.StaticLogic.SecurityLevel(signet) 114 | if err != nil { 115 | if errors.Is(err, tools.ErrProtected) { 116 | return "[protected]" 117 | } 118 | return failPlaceholder 119 | } 120 | 121 | return fmt.Sprintf("%d b/s", securityLevel) 122 | } 123 | 124 | func formatRequirements(reqs *jess.Requirements) string { 125 | if reqs == nil || reqs.Empty() { 126 | return "none (unsafe)" 127 | } 128 | return reqs.String() 129 | } 130 | 131 | func formatSignetNames(signets []*jess.Signet) string { 132 | names := make([]string, 0, len(signets)) 133 | for _, signet := range signets { 134 | names = append(names, formatSignetName(signet)) 135 | } 136 | return strings.Join(names, ", ") 137 | } 138 | 139 | func formatEnvelopeSignets(envelope *jess.Envelope) string { 140 | var sections []string 141 | if len(envelope.Secrets) > 0 { 142 | sections = append(sections, fmt.Sprintf("Secrets: %s", formatSignetNames(envelope.Secrets))) 143 | } 144 | if len(envelope.Recipients) > 0 { 145 | sections = append(sections, fmt.Sprintf("To: %s", formatSignetNames(envelope.Recipients))) 146 | } 147 | if len(envelope.Senders) > 0 { 148 | sections = append(sections, fmt.Sprintf("From: %s", formatSignetNames(envelope.Senders))) 149 | } 150 | return strings.Join(sections, ", ") 151 | } 152 | 153 | func formatSuiteStatus(suite *jess.Suite) string { 154 | switch suite.Status { 155 | case jess.SuiteStatusDeprecated: 156 | return "DEPRECATED" 157 | case jess.SuiteStatusRecommended: 158 | return "recommended" 159 | default: 160 | return "" 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /cmd/format_sig.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | "sort" 7 | "strings" 8 | 9 | "github.com/safing/jess/filesig" 10 | ) 11 | 12 | func formatSignatures(filename, signame string, fds []*filesig.FileData) string { 13 | b := &strings.Builder{} 14 | 15 | switch len(fds) { 16 | case 0: 17 | case 1: 18 | formatSignature(b, fds[0]) 19 | case 2: 20 | for _, fd := range fds { 21 | fmt.Fprintf(b, "%d Signatures:\n\n\n", len(fds)) 22 | formatSignature(b, fd) 23 | b.WriteString("\n\n") 24 | } 25 | } 26 | 27 | if filename != "" || signame != "" { 28 | b.WriteString("\n") 29 | fmt.Fprintf(b, "File: %s\n", filename) 30 | fmt.Fprintf(b, "Sig: %s\n", signame) 31 | } 32 | 33 | return b.String() 34 | } 35 | 36 | func formatSignature(b *strings.Builder, fd *filesig.FileData) { 37 | if fd.VerificationError() == nil { 38 | b.WriteString("Verification: OK\n") 39 | } else { 40 | fmt.Fprintf(b, "Verification FAILED: %s\n", fd.VerificationError()) 41 | } 42 | 43 | if letter := fd.Signature(); letter != nil { 44 | b.WriteString("\n") 45 | for _, sig := range letter.Signatures { 46 | signet, err := trustStore.GetSignet(sig.ID, true) 47 | if err == nil { 48 | fmt.Fprintf(b, "Signed By: %s (%s)\n", signet.Info.Name, sig.ID) 49 | } else { 50 | fmt.Fprintf(b, "Signed By: %s\n", sig.ID) 51 | } 52 | } 53 | } 54 | 55 | if fileHash := fd.FileHash(); fileHash != nil { 56 | b.WriteString("\n") 57 | fmt.Fprintf(b, "Hash Alg: %s\n", fileHash.Algorithm()) 58 | fmt.Fprintf(b, "Hash Sum: %s\n", hex.EncodeToString(fileHash.Sum())) 59 | } 60 | 61 | if len(fd.MetaData) > 0 { 62 | b.WriteString("\nMetadata:\n") 63 | 64 | sortedMetaData := make([][]string, 0, len(fd.MetaData)) 65 | for k, v := range fd.MetaData { 66 | sortedMetaData = append(sortedMetaData, []string{k, v}) 67 | } 68 | sort.Sort(sortByMetaDataKey(sortedMetaData)) 69 | for _, entry := range sortedMetaData { 70 | fmt.Fprintf(b, " %s: %s\n", entry[0], entry[1]) 71 | } 72 | } 73 | } 74 | 75 | type sortByMetaDataKey [][]string 76 | 77 | func (a sortByMetaDataKey) Len() int { return len(a) } 78 | func (a sortByMetaDataKey) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 79 | func (a sortByMetaDataKey) Less(i, j int) bool { return a[i][0] < a[j][0] } 80 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | 7 | "github.com/spf13/cobra" 8 | 9 | "github.com/safing/jess" 10 | _ "github.com/safing/jess/tools/all" 11 | "github.com/safing/jess/truststores" 12 | ) 13 | 14 | const ( 15 | stdInOutFilename = "-" 16 | letterFileExtension = ".letter" 17 | 18 | warnFileSize = 12000000 // 120MB 19 | ) 20 | 21 | var ( 22 | rootCmd = &cobra.Command{ 23 | Use: "jess", 24 | PersistentPreRunE: initGlobalFlags, 25 | RunE: func(cmd *cobra.Command, args []string) error { 26 | return cmd.Help() 27 | }, 28 | SilenceUsage: true, 29 | } 30 | 31 | trustStoreDir string 32 | trustStoreKeyring string 33 | noSpec string 34 | minimumSecurityLevel = 0 35 | defaultSymmetricKeySize = 0 36 | 37 | trustStore truststores.ExtendedTrustStore 38 | requirements *jess.Requirements 39 | ) 40 | 41 | func main() { 42 | rootCmd.PersistentFlags().StringVarP(&trustStoreDir, "tsdir", "d", "", 43 | "specify a truststore directory (default loaded from JESS_TS_DIR env variable)", 44 | ) 45 | rootCmd.PersistentFlags().StringVarP(&trustStoreKeyring, "tskeyring", "r", "", 46 | "specify a truststore keyring namespace (default loaded from JESS_TS_KEYRING env variable) - lower priority than tsdir", 47 | ) 48 | rootCmd.PersistentFlags().StringVarP(&noSpec, "no", "n", "", 49 | "remove requirements using the abbreviations C, I, R, S", 50 | ) 51 | 52 | rootCmd.PersistentFlags().IntVarP(&minimumSecurityLevel, "seclevel", "s", 0, "specify a minimum security level") 53 | rootCmd.PersistentFlags().IntVarP(&defaultSymmetricKeySize, "symkeysize", "k", 0, "specify a default symmetric key size (only applies in certain conditions, use when prompted)") 54 | 55 | if rootCmd.Execute() != nil { 56 | os.Exit(1) 57 | } 58 | os.Exit(0) 59 | } 60 | 61 | func initGlobalFlags(cmd *cobra.Command, args []string) (err error) { 62 | // trust store directory 63 | if trustStoreDir == "" { 64 | trustStoreDir, _ = os.LookupEnv("JESS_TS_DIR") 65 | if trustStoreDir == "" { 66 | trustStoreDir, _ = os.LookupEnv("JESS_TSDIR") 67 | } 68 | } 69 | if trustStoreDir != "" { 70 | trustStore, err = truststores.NewDirTrustStore(trustStoreDir) 71 | if err != nil { 72 | return err 73 | } 74 | } 75 | 76 | // trust store keyring 77 | if trustStore == nil { 78 | if trustStoreKeyring == "" { 79 | trustStoreKeyring, _ = os.LookupEnv("JESS_TS_KEYRING") 80 | if trustStoreKeyring == "" { 81 | trustStoreKeyring, _ = os.LookupEnv("JESS_TSKEYRING") 82 | } 83 | } 84 | if trustStoreKeyring != "" { 85 | trustStore, err = truststores.NewKeyringTrustStore(trustStoreKeyring) 86 | if err != nil { 87 | return err 88 | } 89 | } 90 | } 91 | 92 | // requirements 93 | if noSpec != "" { 94 | requirements, err = jess.ParseRequirementsFromNoSpec(noSpec) 95 | if err != nil { 96 | return err 97 | } 98 | } 99 | 100 | // security level and default key size 101 | if minimumSecurityLevel > 0 { 102 | jess.SetMinimumSecurityLevel(minimumSecurityLevel) 103 | } 104 | if defaultSymmetricKeySize > 0 { 105 | jess.SetDefaultKeySize(defaultSymmetricKeySize) 106 | } 107 | 108 | return nil 109 | } 110 | 111 | func requireTrustStore(cmd *cobra.Command, args []string) error { 112 | if trustStore == nil { 113 | return errors.New("please specify/configure a trust store") 114 | } 115 | return nil 116 | } 117 | -------------------------------------------------------------------------------- /cmd/password.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "crypto/sha1" 6 | "encoding/hex" 7 | "errors" 8 | "fmt" 9 | "log" 10 | "net/http" 11 | "strings" 12 | 13 | "github.com/AlecAivazis/survey/v2" 14 | 15 | "github.com/safing/jess" 16 | ) 17 | 18 | func registerPasswordCallbacks() { 19 | jess.SetPasswordCallbacks(createPasswordInterface, getPasswordInterface) 20 | } 21 | 22 | func getPasswordInterface(signet *jess.Signet) error { 23 | pw, err := getPassword(formatSignetName(signet)) 24 | if err != nil { 25 | return err 26 | } 27 | signet.Key = []byte(pw) 28 | return nil 29 | } 30 | 31 | func createPasswordInterface(signet *jess.Signet, minSecurityLevel int) error { 32 | pw, err := createPassword(formatSignetName(signet), minSecurityLevel) 33 | if err != nil { 34 | return err 35 | } 36 | signet.Key = []byte(pw) 37 | return nil 38 | } 39 | 40 | func getPassword(reference string) (string, error) { 41 | // enter new pw 42 | var pw string 43 | prompt := &survey.Password{ 44 | Message: makePrompt("Please enter password", reference), 45 | } 46 | err := survey.AskOne(prompt, &pw, nil) 47 | if err != nil { 48 | return "", err 49 | } 50 | 51 | return pw, nil 52 | } 53 | 54 | func createPassword(reference string, minSecurityLevel int) (string, error) { 55 | // enter new pw 56 | var pw1 string 57 | prompt := &survey.Password{ 58 | Message: makePrompt("Please enter password", reference), 59 | } 60 | err := survey.AskOne(prompt, &pw1, survey.WithValidator(func(val interface{}) error { 61 | pwVal, ok := val.(string) 62 | if !ok { 63 | return errors.New("input error") 64 | } 65 | // TODO: adapt interations based on tool 66 | pwSecLevel := jess.CalculatePasswordSecurityLevel(pwVal, 20000) 67 | if pwSecLevel < minSecurityLevel { 68 | return fmt.Errorf("please enter a stronger password, you only reached %d bits of security, while the envelope has a minimum of %d", pwSecLevel, minSecurityLevel) 69 | } 70 | return nil 71 | })) 72 | if err != nil { 73 | return "", err 74 | } 75 | // confirm 76 | var pw2 string 77 | prompt = &survey.Password{ 78 | Message: makePrompt("Please confirm password", reference), 79 | } 80 | err = survey.AskOne(prompt, &pw2, nil) 81 | if err != nil { 82 | return "", err 83 | } 84 | 85 | // check match 86 | if pw1 != pw2 { 87 | return "", errors.New("the entered passwords mismatch") 88 | } 89 | 90 | // check password? 91 | check, err := confirm("Do you want to check if the password has been compromised in the past?", false) 92 | if err != nil { 93 | return "", err 94 | } 95 | if check { 96 | err := checkForWeakPassword(pw1) 97 | if err != nil { 98 | return "", err 99 | } 100 | } 101 | 102 | return pw1, nil 103 | } 104 | 105 | func checkForWeakPassword(pw string) error { 106 | // check HIBP 107 | // docs: https://haveibeenpwned.com/API/v2#SearchingPwnedPasswordsByRange 108 | 109 | // hash and split 110 | sum := sha1.Sum([]byte(pw)) //nolint:gosec // required for HIBP API 111 | hexSum := hex.EncodeToString(sum[:]) 112 | prefix := strings.ToUpper(hexSum[:5]) 113 | suffix := strings.ToUpper(hexSum[5:]) 114 | 115 | // request hash list 116 | resp, err := http.Get(fmt.Sprintf("https://api.pwnedpasswords.com/range/%s", prefix)) 117 | if err != nil { 118 | return fmt.Errorf("failed to contact HIBP service: %w", err) 119 | } 120 | defer func() { 121 | _ = resp.Body.Close() 122 | }() 123 | 124 | // check if password is in hash list 125 | bodyReader := bufio.NewReader(resp.Body) 126 | scanner := bufio.NewScanner(bodyReader) 127 | cnt := 0 128 | for scanner.Scan() { 129 | if strings.HasPrefix(scanner.Text(), suffix) { 130 | log.Printf("%+v", scanner.Text()) 131 | fields := strings.Split(scanner.Text(), ":") 132 | log.Printf("%+v", fields) 133 | if len(fields) >= 2 { 134 | //nolint:golint,stylecheck // is user error message 135 | return fmt.Errorf("password detected in HIBP database - it has been leaked %s times!", fields[1]) 136 | } 137 | //nolint:golint,stylecheck // is user error message 138 | return errors.New("password detected in HIBP database - it has been leaked!") 139 | } 140 | cnt++ 141 | } 142 | // fmt.Printf("checked %d leaked passwords\n", cnt) 143 | if err := scanner.Err(); err != nil { 144 | return fmt.Errorf("failed to read HIBP response: %w", err) 145 | } 146 | 147 | return nil 148 | } 149 | 150 | func makePrompt(prompt, reference string) string { 151 | if reference != "" { 152 | return fmt.Sprintf(`%s "%s":`, prompt, reference) 153 | } 154 | return fmt.Sprintf("%s:", prompt) 155 | } 156 | -------------------------------------------------------------------------------- /cmd/password_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | //nolint:unused,deadcode // tested manually 8 | func testCfWP(t *testing.T, password string, expectedError string) { 9 | t.Helper() 10 | 11 | var errMsg string 12 | err := checkForWeakPassword(password) 13 | if err != nil { 14 | errMsg = err.Error() 15 | } 16 | if errMsg != expectedError { 17 | t.Errorf(`expected error "%s", got: "%s"`, expectedError, errMsg) 18 | } 19 | } 20 | 21 | func TestCheckForWeakPassword(t *testing.T) { 22 | t.Parallel() 23 | 24 | // TODO: only run these manually, as they actually require the live HIBP API. 25 | // testCfWP(t, "asdfasdfasdf", "") 26 | // testCfWP(t, "mfJLiQH9O9V9zXYrkNeYvGLvE14HcPyW7/sWWGfBX2nBU7c", "") 27 | } 28 | -------------------------------------------------------------------------------- /cmd/testdata/.truststore/3911c84c-78f7-4354-a7f5-0e115aa2903c.recipient: -------------------------------------------------------------------------------- 1 | J{ 2 | "Version": 1, 3 | "ID": "3911c84c-78f7-4354-a7f5-0e115aa2903c", 4 | "Scheme": "Ed25519", 5 | "Key": "ATYVZjmhR1Zwe0KAPV99pzbzI+6zWgKvELNhFwolRdnv", 6 | "Public": true, 7 | "Info": { 8 | "Name": "Safing Code Signing Cert 1", 9 | "Owner": "", 10 | "Created": "2022-07-11T10:23:31.705715613+02:00", 11 | "Expires": "0001-01-01T00:00:00Z", 12 | "Details": null 13 | } 14 | } -------------------------------------------------------------------------------- /cmd/testdata/.truststore/3911c84c-78f7-4354-a7f5-0e115aa2903c.signet: -------------------------------------------------------------------------------- 1 | J{ 2 | "Version": 1, 3 | "ID": "3911c84c-78f7-4354-a7f5-0e115aa2903c", 4 | "Scheme": "Ed25519", 5 | "Key": "Aee5n/V1wJM8aNpaNEPBEPeN6S0Tl41OJP0rHwtsGcZcNhVmOaFHVnB7QoA9X32nNvMj7rNaAq8Qs2EXCiVF2e8=", 6 | "Info": { 7 | "Name": "Safing Code Signing Cert 1", 8 | "Owner": "", 9 | "Created": "2022-07-11T10:23:31.705715613+02:00", 10 | "Expires": "0001-01-01T00:00:00Z", 11 | "Details": null 12 | } 13 | } -------------------------------------------------------------------------------- /cmd/testdata/.truststore/safing-codesign-1.envelope: -------------------------------------------------------------------------------- 1 | J{ 2 | "Version": 1, 3 | "Name": "safing-codesign-1", 4 | "SuiteID": "signfile_v1", 5 | "Secrets": null, 6 | "Senders": [ 7 | { 8 | "Version": 1, 9 | "ID": "3911c84c-78f7-4354-a7f5-0e115aa2903c", 10 | "Scheme": "Ed25519", 11 | "Key": null, 12 | "Info": { 13 | "Name": "Safing Code Signing Cert 1", 14 | "Owner": "", 15 | "Created": "2022-07-11T10:23:31.705715613+02:00", 16 | "Expires": "0001-01-01T00:00:00Z", 17 | "Details": null 18 | } 19 | } 20 | ], 21 | "Recipients": null, 22 | "SecurityLevel": 128 23 | } -------------------------------------------------------------------------------- /cmd/testdata/test.txt: -------------------------------------------------------------------------------- 1 | hello world! 2 | -------------------------------------------------------------------------------- /cmd/testdata/test.txt.letter: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/jess/4c4b4471d8e6820b934b007035fbc53b707937d4/cmd/testdata/test.txt.letter -------------------------------------------------------------------------------- /cmd/testdata/test.txt.sig: -------------------------------------------------------------------------------- 1 | -----BEGIN JESS SIGNATURE----- 2 | Q6VnVmVyc2lvbgFnU3VpdGVJRGtzaWduZmlsZV92MWVOb25jZUQb+MqAZERhdGFY 3 | d02Dq0xhYmVsZWRIYXNoxCIJIOz3Afcn2eLXfEqkmsb7vMmXJ4rKAQvd7rlhwQz1 4 | TUNaqFNpZ25lZEF01v9iy+uLqE1ldGFEYXRhgqd2ZXJzaW9upTAuMC4xqmlkZW50 5 | aWZpZXKyd2luZG93cy9jb2RlL3RoaW5nalNpZ25hdHVyZXOBo2ZTY2hlbWVnRWQy 6 | NTUxOWJJRHgkMzkxMWM4NGMtNzhmNy00MzU0LWE3ZjUtMGUxMTVhYTI5MDNjZVZh 7 | bHVlWECJZFbIifczUGAJkmATXCHy/MiQZkiktM99X7U/cPgw3IKpKAxQsJ5LobgZ 8 | 4P2ecv0IlN4gQb+x+lycxl93E9sJ 9 | -----END JESS SIGNATURE----- -------------------------------------------------------------------------------- /cmd/testdata/test3.txt: -------------------------------------------------------------------------------- 1 | hello world!! 2 | -------------------------------------------------------------------------------- /cmd/testdata/test3.txt.sig: -------------------------------------------------------------------------------- 1 | -----BEGIN JESS SIGNATURE----- 2 | Q6VnVmVyc2lvbgFnU3VpdGVJRGtzaWduZmlsZV92MWVOb25jZUQJ9s/nZERhdGFY 3 | d02Dq0xhYmVsZWRIYXNoxCIJILtKnL1AHj7YubrWdLu1D+voud8Ky04vh756eTae 4 | rWQwqFNpZ25lZEF01v9izC6hqE1ldGFEYXRhgqd2ZXJzaW9upTAuMC4xqmlkZW50 5 | aWZpZXKyd2luZG93cy9jb2RlL3RoaW5nalNpZ25hdHVyZXOBo2ZTY2hlbWVnRWQy 6 | NTUxOWJJRHgkMzkxMWM4NGMtNzhmNy00MzU0LWE3ZjUtMGUxMTVhYTI5MDNjZVZh 7 | bHVlWEBLsd2QbM7VmEsnW60hHn/V6EP2mGFauWZgbEOlKTiqumVFbWU4K7Fi91KL 8 | Zgvwj+CNdZJ7Xv2qR7etviRDCmwC 9 | -----END JESS SIGNATURE----- -------------------------------------------------------------------------------- /cmd/testdata/test4.txt: -------------------------------------------------------------------------------- 1 | hello world! 2 | -------------------------------------------------------------------------------- /cmd/testdata/testdir/test2.txt: -------------------------------------------------------------------------------- 1 | hello world! 2 | -------------------------------------------------------------------------------- /cmd/testdata/testdir/test2.txt.sig: -------------------------------------------------------------------------------- 1 | -----BEGIN JESS SIGNATURE----- 2 | Q6VnVmVyc2lvbgFnU3VpdGVJRGtzaWduZmlsZV92MWVOb25jZUThzxO6ZERhdGFY 3 | d02Dq0xhYmVsZWRIYXNoxCIJIOz3Afcn2eLXfEqkmsb7vMmXJ4rKAQvd7rlhwQz1 4 | TUNaqFNpZ25lZEF01v9izC3SqE1ldGFEYXRhgqd2ZXJzaW9upTAuMC4xqmlkZW50 5 | aWZpZXKyd2luZG93cy9jb2RlL3RoaW5nalNpZ25hdHVyZXOBo2ZTY2hlbWVnRWQy 6 | NTUxOWJJRHgkMzkxMWM4NGMtNzhmNy00MzU0LWE3ZjUtMGUxMTVhYTI5MDNjZVZh 7 | bHVlWEAGLkIoej0+ilJrIyb+BzX8+Yw2LY0zkoL9vwI02/2KqKVT7/pH+LTDX1Hl 8 | h1epYkF8ICdwa1iVNDx6P7iNmWkL 9 | -----END JESS SIGNATURE----- -------------------------------------------------------------------------------- /cmd/utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/AlecAivazis/survey/v2" 5 | ) 6 | 7 | func confirm(promptMsg string, suggest bool) (bool, error) { 8 | confirmed := suggest 9 | prompt := &survey.Confirm{ 10 | Message: promptMsg, 11 | Default: suggest, 12 | } 13 | err := survey.AskOne(prompt, &confirmed, nil) 14 | if err != nil { 15 | return false, err 16 | } 17 | return confirmed, nil 18 | } 19 | -------------------------------------------------------------------------------- /defaults.go: -------------------------------------------------------------------------------- 1 | package jess 2 | 3 | var ( 4 | // Must be var in order decrease for testing for better speed. 5 | 6 | defaultSecurityLevel = 128 7 | minimumSecurityLevel = 0 8 | 9 | defaultSymmetricKeySize = 16 10 | minimumSymmetricKeySize = 0 11 | ) 12 | 13 | // Currently recommended toolsets. 14 | var ( 15 | RecommendedNetwork = []string{"ECDH-X25519", "HKDF(SHA2-256)", "CHACHA20-POLY1305"} 16 | RecommendedStoragePassword = []string{"PBKDF2-SHA2-256", "HKDF(SHA2-256)", "CHACHA20-POLY1305"} 17 | RecommendedStorageKey = []string{"HKDF(SHA2-256)", "CHACHA20-POLY1305"} 18 | 19 | RecommendedStorageRecipient = []string{"ECDH-X25519", "HKDF(SHA2-256)", "CHACHA20-POLY1305"} 20 | 21 | RecommendedSigning = []string{"Ed25519(SHA2-256)"} 22 | ) 23 | 24 | // SetMinimumSecurityLevel sets a global minimum security level. Jess will refuse any operations that violate this security level. 25 | func SetMinimumSecurityLevel(securityLevel int) { 26 | defaultSecurityLevel = securityLevel 27 | minimumSecurityLevel = securityLevel 28 | } 29 | 30 | // SetDefaultKeySize sets a global default key size to be used as a fallback value. This will be only used if the default key size could not be derived from already present information. 31 | func SetDefaultKeySize(sizeInBytes int) { 32 | defaultSymmetricKeySize = sizeInBytes 33 | minimumSymmetricKeySize = sizeInBytes 34 | } 35 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | jess is a lovely cat that protects your data. 3 | 4 | Jess uses four types of objects: 5 | - Envelopes (encryption configuration) 6 | - Letters (encrypted data) 7 | - Stamp (private or secret key) 8 | - Signet (certificate / public key) 9 | - Seal (separate signature) 10 | 11 | Usage: 12 | 13 | message := "I love milk" 14 | 15 | // configure 16 | envelope, err := jess.NewEnvelope().SupplyPassword("paw").Check() 17 | 18 | // encrypt 19 | letter := jess.Close(envelope, message) 20 | encrypted := letter.AsString() 21 | fmt.Println(encrypted) 22 | 23 | // decrypt 24 | letter = jess.LetterFromString(encrypted) 25 | message = jess.Open(envelope, letter) 26 | fmt.Println(message) 27 | 28 | CLI: coming soon 29 | 30 | jess new 31 | // create new configuration 32 | 33 | jess close with 34 | // encrypt data in a letter 35 | 36 | jess open 37 | // decrypt and verify letter 38 | 39 | jess show 40 | // show information about object 41 | 42 | jess sign with 43 | // special close case, where only a signature is created and put in a separate `.seal` file 44 | 45 | Internals: 46 | 47 | Envelope.Correspondence() *Session 48 | 49 | 50 | 51 | Key Establishment 52 | 53 | 54 | Exchange: 55 | 56 | c=IDLE s=IDLE 57 | 58 | c -> new ephemeral public key -> s 59 | ... detected by len(keys) > 0 60 | c=AWAIT_KEY, s=SEND_KEY 61 | 62 | s: make new ephemeral key, apply new shared secret immediately 63 | s -> new ephemeral public key -> c 64 | ... detected by len(keys) > 0 65 | c: apply new shared secret immediately for s->c 66 | c=SEND_APPLY, s=AWAIT_APPLY 67 | 68 | c: apply new shared secret to c->s 69 | c -> apply -> s 70 | ... detected by APPLY flag 71 | s: apply to c->s 72 | c=IDLE, S=IDLE 73 | 74 | Encapsulation: 75 | 76 | c=IDLE s=IDLE 77 | 78 | c -> new ephemeral public key -> s 79 | ... detected by len(keys) > 0 80 | c=AWAIT_KEY, s=SEND_KEY 81 | 82 | s: make key, apply immediately and encapsulate 83 | s -> encapsulated key -> c 84 | ... detected by len(keys) > 0 85 | c: apply encapsulated key immediately for s->c 86 | c=SEND_APPLY, s=AWAIT_APPLY 87 | 88 | c: apply encapsulated secret for c->s 89 | c -> apply -> s 90 | ... detected by APPLY flag 91 | s: apply to c->s 92 | c=IDLE, S=IDLE 93 | 94 | */ 95 | 96 | package jess 97 | -------------------------------------------------------------------------------- /docs/AUDITS.md: -------------------------------------------------------------------------------- 1 | # Audits 2 | 3 | ## Audit #001 4 | 5 | In January 2020, the project was audited by [Cure53](https://cure53.de/). 6 | Cure53 was chosen because they have proven to both excell as auditors as well as being committed to building a more open and better Internet - I mean, just have a look at their [publications](https://cure53.de/#publications)! 7 | 8 | [This commit](https://github.com/safing/jess/commit/648d16d1cc8185ed3704373e43c84a6ab4315498) was handed in for the audit. 9 | Fixes can be found in [PR #3](https://github.com/safing/jess/pull/3) and was merged [here](https://github.com/safing/jess/commit/41fbc87f119a7d69f0fd9f24275e245fd4e2eedf). 10 | 11 | They found 5 issues: 12 | 13 | __Secure key deletion ineffective (Medium Severity)__ 14 | Golang does not yet provide a secure way of handling key material. The is no clean fix, we were advised to wait. Documentation has been updated to reflect this. 15 | See [Github issue](https://github.com/golang/go/issues/21865) for details. 16 | 17 | __Password KDF vulnerable to GPU/ASIC attacks (Medium Severity)__ 18 | PBKDF2 is vulnarable to GPU/ASIC attacks, was replaced with scrypt with a much higher security margin (rounds). 19 | 20 | __Secure channel protocol weaknesses (High Severity)__ 21 | Verification of the protocol with [Verifpal](https://verifpal.com) revealed that in addition to one expected weakness, there is another. The found weakness should actually have been expected, because it is a limitation of the protocol. The main use case of the protocol, securing SPN connections, is not impacted. Documentation was updated. 22 | 23 | __Key management/encryption with 1-byte key (Critical Severity)__ 24 | This was just a devops error. We forgot to replace a "FIXME" comment with a function call. 🙈 25 | 26 | __Unnecessary configurability considered dangerous (Medium Severity)__ 27 | This was somewhat expected. We did not yet know how to best expose the configurability to users. We were advised: NOT. We implemented changes and introduced cipher suites that specify a fixed sets of algorithms and security guarantees. 28 | 29 | The full report is available [in-repo here](audit_001_report_cure53_SAF-01.pdf) or [directly from the auditor](https://cure53.de/pentest-report_safing-jess.pdf). 30 | 31 | # Formal Verification 32 | 33 | In the first Audit by Cure53, one of the auditors, [Nadim Kobeissi](https://nadim.computer/), used his software [Verifpal](https://verifpal.com/) for an automated formal verficiation of the wire protocol. This was quite an amazing thing, as we wrote the model definition _in_ the kickoff meeting. Verifpal then combed through the model to check if it really holds up to its promises. You can find the model [here](key_establishment_dh.vp). 34 | -------------------------------------------------------------------------------- /docs/audit_001_report_cure53_SAF-01.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safing/jess/4c4b4471d8e6820b934b007035fbc53b707937d4/docs/audit_001_report_cure53_SAF-01.pdf -------------------------------------------------------------------------------- /docs/key_establishment_dh.vp: -------------------------------------------------------------------------------- 1 | attacker[active] 2 | 3 | principal Client[ 4 | generates e1 5 | ge1 = G^e1 6 | ] 7 | 8 | principal Server[ 9 | knows private s 10 | generates se 11 | gse = G^se 12 | gs = G^s 13 | gseSignature = SIGN(s, gse) 14 | ] 15 | 16 | Server -> Client: [gs], gse, gseSignature 17 | 18 | principal Client[ 19 | _ = SIGNVERIF(gs, gse, gseSignature)? 20 | knows private msg1 21 | s1c = gse^e1 22 | enc1 = AEAD_ENC(s1c, msg1, HASH(gs, gse, ge1)) 23 | ] 24 | 25 | Client -> Server: ge1, enc1 26 | 27 | principal Server[ 28 | s1s = ge1^se 29 | dec1 = AEAD_DEC(s1s, enc1, HASH(gs, gse, ge1))? 30 | generates e2 31 | s2s = ge1^e2 32 | ge2 = G^e2 33 | s3s = HKDF(s1s, s2s, nil) 34 | knows private msg2 35 | enc2 = AEAD_ENC(s3s, msg2, HASH(gs, gse, ge1, ge2)) 36 | ] 37 | 38 | Server -> Client: ge2, enc2 39 | 40 | principal Client[ 41 | s2c = ge2^e1 42 | s3c = HKDF(s1c, s2c, nil) 43 | dec2 = AEAD_DEC(s3c, enc2, HASH(gs, gse, ge1, ge2))? 44 | ] 45 | 46 | queries[ 47 | confidentiality? msg1 48 | confidentiality? msg2 49 | authentication? Client -> Server: enc1 50 | authentication? Server -> Client: enc2 51 | ] 52 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package jess 2 | 3 | import "errors" 4 | 5 | var ( 6 | // ErrIntegrityViolation is returned when the integrity was found the be violated. 7 | ErrIntegrityViolation = errors.New("integrity violation") 8 | // ErrConfidentialityViolation is returned when the confidentiality was found the be violated. 9 | ErrConfidentialityViolation = errors.New("confidentiality violation") 10 | // ErrAuthenticityViolation is returned when the authenticity was found the be violated. 11 | ErrAuthenticityViolation = errors.New("authenticity violation") 12 | 13 | // ErrInsufficientRandom is returned if the configured RNG cannot deliver enough data. 14 | ErrInsufficientRandom = errors.New("not enough random data available from source") 15 | ) 16 | -------------------------------------------------------------------------------- /filesig/format_armor.go: -------------------------------------------------------------------------------- 1 | package filesig 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "fmt" 7 | "regexp" 8 | 9 | "github.com/safing/jess" 10 | "github.com/safing/structures/dsd" 11 | ) 12 | 13 | const ( 14 | sigFileArmorStart = "-----BEGIN JESS SIGNATURE-----" 15 | sigFileArmorEnd = "-----END JESS SIGNATURE-----" 16 | sigFileLineLength = 64 17 | ) 18 | 19 | var ( 20 | sigFileArmorFindMatcher = regexp.MustCompile(`(?ms)` + sigFileArmorStart + `(.+?)` + sigFileArmorEnd) 21 | sigFileArmorRemoveMatcher = regexp.MustCompile(`(?ms)` + sigFileArmorStart + `.+?` + sigFileArmorEnd + `\r?\n?`) 22 | whitespaceMatcher = regexp.MustCompile(`(?ms)\s`) 23 | ) 24 | 25 | // ParseSigFile parses a signature file and extracts any jess signatures from it. 26 | // If signatures are returned along with an error, the error should be treated 27 | // as a warning, but the result should also not be treated as a full success, 28 | // as there might be missing signatures. 29 | func ParseSigFile(fileData []byte) (signatures []*jess.Letter, err error) { 30 | var warning error 31 | captured := make([][]byte, 0, 1) 32 | 33 | // Find any signature blocks. 34 | matches := sigFileArmorFindMatcher.FindAllSubmatch(fileData, -1) 35 | for _, subMatches := range matches { 36 | if len(subMatches) >= 2 { 37 | // First entry is the whole match, second the submatch. 38 | captured = append( 39 | captured, 40 | bytes.TrimPrefix( 41 | bytes.TrimSuffix( 42 | whitespaceMatcher.ReplaceAll(subMatches[1], nil), 43 | []byte(sigFileArmorEnd), 44 | ), 45 | []byte(sigFileArmorStart), 46 | ), 47 | ) 48 | } 49 | } 50 | 51 | // Parse any found signatures. 52 | signatures = make([]*jess.Letter, 0, len(captured)) 53 | for _, sigBase64Data := range captured { 54 | // Decode from base64 55 | sigData := make([]byte, base64.RawStdEncoding.DecodedLen(len(sigBase64Data))) 56 | _, err = base64.RawStdEncoding.Decode(sigData, sigBase64Data) 57 | if err != nil { 58 | warning = err 59 | continue 60 | } 61 | 62 | // Parse signature. 63 | var letter *jess.Letter 64 | letter, err = jess.LetterFromDSD(sigData) 65 | if err != nil { 66 | warning = err 67 | } else { 68 | signatures = append(signatures, letter) 69 | } 70 | } 71 | 72 | return signatures, warning 73 | } 74 | 75 | // MakeSigFileSection creates a new section for a signature file. 76 | func MakeSigFileSection(signature *jess.Letter) ([]byte, error) { 77 | // Serialize. 78 | data, err := signature.ToDSD(dsd.CBOR) 79 | if err != nil { 80 | return nil, fmt.Errorf("failed to serialize signature: %w", err) 81 | } 82 | 83 | // Encode to base64 84 | encodedData := make([]byte, base64.RawStdEncoding.EncodedLen(len(data))) 85 | base64.RawStdEncoding.Encode(encodedData, data) 86 | 87 | // Split into lines and add armor. 88 | splittedData := make([][]byte, 0, (len(encodedData)/sigFileLineLength)+3) 89 | splittedData = append(splittedData, []byte(sigFileArmorStart)) 90 | for len(encodedData) > 0 { 91 | if len(encodedData) > sigFileLineLength { 92 | splittedData = append(splittedData, encodedData[:sigFileLineLength]) 93 | encodedData = encodedData[sigFileLineLength:] 94 | } else { 95 | splittedData = append(splittedData, encodedData) 96 | encodedData = nil 97 | } 98 | } 99 | splittedData = append(splittedData, []byte(sigFileArmorEnd)) 100 | linedData := bytes.Join(splittedData, []byte("\n")) 101 | 102 | return linedData, nil 103 | } 104 | 105 | // AddToSigFile adds the given signature to the signature file. 106 | func AddToSigFile(signature *jess.Letter, sigFileData []byte, removeExistingJessSignatures bool) (newFileData []byte, err error) { 107 | // Create new section for new sig. 108 | newSigSection, err := MakeSigFileSection(signature) 109 | if err != nil { 110 | return nil, err 111 | } 112 | 113 | // Remove any existing jess signature sections. 114 | if removeExistingJessSignatures { 115 | sigFileData = sigFileArmorRemoveMatcher.ReplaceAll(sigFileData, nil) 116 | } 117 | 118 | // Append new signature section to end of file with a newline. 119 | sigFileData = append(sigFileData, []byte("\n")...) 120 | sigFileData = append(sigFileData, newSigSection...) 121 | 122 | return sigFileData, nil 123 | } 124 | -------------------------------------------------------------------------------- /filesig/helpers.go: -------------------------------------------------------------------------------- 1 | package filesig 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io/fs" 7 | "os" 8 | "strings" 9 | 10 | "github.com/safing/jess" 11 | "github.com/safing/jess/hashtools" 12 | "github.com/safing/jess/lhash" 13 | ) 14 | 15 | // SignFile signs a file and replaces the signature file with a new one. 16 | // If the dataFilePath is "-", the file data is read from stdin. 17 | // Existing jess signatures in the signature file are removed. 18 | func SignFile(dataFilePath, signatureFilePath string, metaData map[string]string, envelope *jess.Envelope, trustStore jess.TrustStore) (fileData *FileData, err error) { 19 | // Load encryption suite. 20 | if err := envelope.LoadSuite(); err != nil { 21 | return nil, err 22 | } 23 | 24 | // Extract the used hashing algorithm from the suite. 25 | var hashTool *hashtools.HashTool 26 | for _, toolID := range envelope.Suite().Tools { 27 | if strings.Contains(toolID, "(") { 28 | hashToolID := strings.Trim(strings.Split(toolID, "(")[1], "()") 29 | hashTool, _ = hashtools.Get(hashToolID) 30 | break 31 | } 32 | } 33 | if hashTool == nil { 34 | return nil, errors.New("suite not suitable for file signing") 35 | } 36 | 37 | // Hash the data file. 38 | var fileHash *lhash.LabeledHash 39 | if dataFilePath == "-" { 40 | fileHash, err = hashTool.LabeledHasher().DigestFromReader(os.Stdin) 41 | } else { 42 | fileHash, err = hashTool.LabeledHasher().DigestFile(dataFilePath) 43 | } 44 | if err != nil { 45 | return nil, fmt.Errorf("failed to hash file: %w", err) 46 | } 47 | 48 | // Sign the file data. 49 | signature, fileData, err := SignFileData(fileHash, metaData, envelope, trustStore) 50 | if err != nil { 51 | return nil, fmt.Errorf("failed to sign file: %w", err) 52 | } 53 | 54 | sigFileData, err := os.ReadFile(signatureFilePath) 55 | var newSigFileData []byte 56 | switch { 57 | case err == nil: 58 | // Add signature to existing file. 59 | newSigFileData, err = AddToSigFile(signature, sigFileData, true) 60 | if err != nil { 61 | return nil, fmt.Errorf("failed to add signature to file: %w", err) 62 | } 63 | case errors.Is(err, fs.ErrNotExist): 64 | // Make signature section for saving to disk. 65 | newSigFileData, err = MakeSigFileSection(signature) 66 | if err != nil { 67 | return nil, fmt.Errorf("failed to format signature for file: %w", err) 68 | } 69 | default: 70 | return nil, fmt.Errorf("failed to open existing signature file: %w", err) 71 | } 72 | 73 | // Write the signature to file. 74 | if err := os.WriteFile(signatureFilePath, newSigFileData, 0o0644); err != nil { //nolint:gosec 75 | return nil, fmt.Errorf("failed to write signature to file: %w", err) 76 | } 77 | 78 | return fileData, nil 79 | } 80 | 81 | // VerifyFile verifies the given files and returns the verified file data. 82 | // If the dataFilePath is "-", the file data is read from stdin. 83 | // If an error is returned, there was an error in at least some part of the process. 84 | // Any returned file data struct must be checked for an verification error. 85 | func VerifyFile(dataFilePath, signatureFilePath string, metaData map[string]string, trustStore jess.TrustStore) (verifiedFileData []*FileData, err error) { 86 | var lastErr error 87 | 88 | // Read signature from file. 89 | sigFileData, err := os.ReadFile(signatureFilePath) 90 | if err != nil { 91 | return nil, fmt.Errorf("failed to read signature file: %w", err) 92 | } 93 | 94 | // Extract all signatures. 95 | sigs, err := ParseSigFile(sigFileData) 96 | switch { 97 | case len(sigs) == 0 && err != nil: 98 | return nil, fmt.Errorf("failed to parse signature file: %w", err) 99 | case len(sigs) == 0: 100 | return nil, errors.New("no signatures found in signature file") 101 | case err != nil: 102 | lastErr = fmt.Errorf("failed to parse signature file: %w", err) 103 | } 104 | 105 | // Verify all signatures. 106 | goodFileData := make([]*FileData, 0, len(sigs)) 107 | var badFileData []*FileData 108 | for _, sigLetter := range sigs { 109 | // Verify signature. 110 | fileData, err := VerifyFileData(sigLetter, metaData, trustStore) 111 | if err != nil { 112 | lastErr = err 113 | if fileData != nil { 114 | fileData.verificationError = err 115 | badFileData = append(badFileData, fileData) 116 | } 117 | continue 118 | } 119 | 120 | // Hash the file. 121 | var fileHash *lhash.LabeledHash 122 | if dataFilePath == "-" { 123 | fileHash, err = fileData.FileHash().Algorithm().DigestFromReader(os.Stdin) 124 | } else { 125 | fileHash, err = fileData.FileHash().Algorithm().DigestFile(dataFilePath) 126 | } 127 | if err != nil { 128 | lastErr = err 129 | fileData.verificationError = err 130 | badFileData = append(badFileData, fileData) 131 | continue 132 | } 133 | 134 | // Check if the hash matches. 135 | if !fileData.FileHash().Equal(fileHash) { 136 | lastErr = errors.New("signature invalid: file was modified") 137 | fileData.verificationError = lastErr 138 | badFileData = append(badFileData, fileData) 139 | continue 140 | } 141 | 142 | // Add verified file data to list for return. 143 | goodFileData = append(goodFileData, fileData) 144 | } 145 | 146 | return append(goodFileData, badFileData...), lastErr 147 | } 148 | -------------------------------------------------------------------------------- /filesig/main.go: -------------------------------------------------------------------------------- 1 | package filesig 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/safing/jess" 8 | "github.com/safing/jess/lhash" 9 | "github.com/safing/structures/dsd" 10 | ) 11 | 12 | // Extension holds the default file extension to be used for signature files. 13 | const Extension = ".sig" 14 | 15 | var fileSigRequirements = jess.NewRequirements(). 16 | Remove(jess.RecipientAuthentication). 17 | Remove(jess.Confidentiality) 18 | 19 | // FileData describes a file that is signed. 20 | type FileData struct { 21 | LabeledHash []byte 22 | fileHash *lhash.LabeledHash 23 | 24 | SignedAt time.Time 25 | MetaData map[string]string 26 | 27 | signature *jess.Letter 28 | verificationError error 29 | } 30 | 31 | // FileHash returns the labeled hash of the file that was signed. 32 | func (fd *FileData) FileHash() *lhash.LabeledHash { 33 | return fd.fileHash 34 | } 35 | 36 | // Signature returns the signature, if present. 37 | func (fd *FileData) Signature() *jess.Letter { 38 | return fd.signature 39 | } 40 | 41 | // VerificationError returns the error encountered during verification. 42 | func (fd *FileData) VerificationError() error { 43 | return fd.verificationError 44 | } 45 | 46 | // SignFileData signs the given file checksum and metadata. 47 | func SignFileData(fileHash *lhash.LabeledHash, metaData map[string]string, envelope *jess.Envelope, trustStore jess.TrustStore) (letter *jess.Letter, fd *FileData, err error) { 48 | // Create session. 49 | session, err := envelope.Correspondence(trustStore) 50 | if err != nil { 51 | return nil, nil, err 52 | } 53 | 54 | // Check if the envelope is suitable for signing. 55 | if err := envelope.Suite().Provides.CheckComplianceTo(fileSigRequirements); err != nil { 56 | return nil, nil, fmt.Errorf("envelope not suitable for signing: %w", err) 57 | } 58 | 59 | // Create struct and transform data into serializable format to be signed. 60 | fd = &FileData{ 61 | SignedAt: time.Now().Truncate(time.Second), 62 | fileHash: fileHash, 63 | MetaData: metaData, 64 | } 65 | fd.LabeledHash = fd.fileHash.Bytes() 66 | 67 | // Serialize file signature. 68 | fileData, err := dsd.Dump(fd, dsd.MsgPack) 69 | if err != nil { 70 | return nil, nil, fmt.Errorf("failed to serialize file signature data: %w", err) 71 | } 72 | 73 | // Sign data. 74 | letter, err = session.Close(fileData) 75 | if err != nil { 76 | return nil, nil, fmt.Errorf("failed to sign: %w", err) 77 | } 78 | 79 | return letter, fd, nil 80 | } 81 | 82 | // VerifyFileData verifies the given signed file data and returns the file data. 83 | // If an error is returned, there was an error in at least some part of the process. 84 | // Any returned file data struct must be checked for an verification error. 85 | func VerifyFileData(letter *jess.Letter, requiredMetaData map[string]string, trustStore jess.TrustStore) (fd *FileData, err error) { 86 | // Parse data. 87 | fd = &FileData{ 88 | signature: letter, 89 | } 90 | _, err = dsd.Load(letter.Data, fd) 91 | if err != nil { 92 | return nil, fmt.Errorf("failed to parse file signature data: %w", err) 93 | } 94 | 95 | // Verify signature and get data. 96 | _, err = letter.Open(fileSigRequirements, trustStore) 97 | if err != nil { 98 | fd.verificationError = fmt.Errorf("failed to verify file signature: %w", err) 99 | return fd, fd.verificationError 100 | } 101 | 102 | // Check if the required metadata matches. 103 | for reqKey, reqValue := range requiredMetaData { 104 | sigMetaValue, ok := fd.MetaData[reqKey] 105 | if !ok { 106 | fd.verificationError = fmt.Errorf("missing required metadata key %q", reqKey) 107 | return fd, fd.verificationError 108 | } 109 | if sigMetaValue != reqValue { 110 | fd.verificationError = fmt.Errorf("required metadata %q=%q does not match the file's value %q", reqKey, reqValue, sigMetaValue) 111 | return fd, fd.verificationError 112 | } 113 | } 114 | 115 | // Parse labeled hash. 116 | fd.fileHash, err = lhash.Load(fd.LabeledHash) 117 | if err != nil { 118 | fd.verificationError = fmt.Errorf("failed to parse file checksum: %w", err) 119 | return fd, fd.verificationError 120 | } 121 | 122 | return fd, nil 123 | } 124 | -------------------------------------------------------------------------------- /filesig/main_test.go: -------------------------------------------------------------------------------- 1 | package filesig 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | "time" 7 | 8 | "github.com/safing/jess" 9 | "github.com/safing/jess/lhash" 10 | "github.com/safing/jess/tools" 11 | ) 12 | 13 | var ( 14 | testTrustStore = jess.NewMemTrustStore() 15 | testData1 = "The quick brown fox jumps over the lazy dog. " 16 | 17 | testFileSigMetaData1 = map[string]string{ 18 | "key1": "value1", 19 | "key2": "value2", 20 | } 21 | testFileSigMetaData1x = map[string]string{ 22 | "key1": "value1x", 23 | } 24 | testFileSigMetaData2 = map[string]string{ 25 | "key3": "value3", 26 | "key4": "value4", 27 | } 28 | testFileSigMetaData3 = map[string]string{} 29 | ) 30 | 31 | func TestFileSigs(t *testing.T) { 32 | t.Parallel() 33 | 34 | testFileSigningWithOptions(t, testFileSigMetaData1, testFileSigMetaData1, true) 35 | testFileSigningWithOptions(t, testFileSigMetaData1, testFileSigMetaData1x, false) 36 | testFileSigningWithOptions(t, testFileSigMetaData2, testFileSigMetaData2, true) 37 | testFileSigningWithOptions(t, testFileSigMetaData1, testFileSigMetaData2, false) 38 | testFileSigningWithOptions(t, testFileSigMetaData2, testFileSigMetaData1, false) 39 | testFileSigningWithOptions(t, testFileSigMetaData1, testFileSigMetaData3, true) 40 | testFileSigningWithOptions(t, testFileSigMetaData3, testFileSigMetaData1, false) 41 | } 42 | 43 | func testFileSigningWithOptions(t *testing.T, signingMetaData, verifyingMetaData map[string]string, shouldSucceed bool) { 44 | t.Helper() 45 | 46 | // Get tool for key generation. 47 | tool, err := tools.Get("Ed25519") 48 | if err != nil { 49 | t.Fatal(err) 50 | } 51 | 52 | // Generate key pair. 53 | s, err := getOrMakeSignet(t, tool.StaticLogic, false, "test-key-filesig-1") 54 | if err != nil { 55 | t.Fatal(err) 56 | } 57 | 58 | // Hash "file". 59 | fileHash := lhash.BLAKE2b_256.Digest([]byte(testData1)) 60 | 61 | // Make envelope. 62 | envelope := jess.NewUnconfiguredEnvelope() 63 | envelope.SuiteID = jess.SuiteSignV1 64 | envelope.Senders = []*jess.Signet{s} 65 | 66 | // Sign data. 67 | letter, fileData, err := SignFileData(fileHash, signingMetaData, envelope, testTrustStore) 68 | if err != nil { 69 | t.Fatal(err) 70 | } 71 | 72 | // Check if the checksum made it. 73 | if len(fileData.LabeledHash) == 0 { 74 | t.Fatal("missing labeled hash") 75 | } 76 | 77 | // Verify signature. 78 | _, err = VerifyFileData(letter, verifyingMetaData, testTrustStore) 79 | if (err == nil) != shouldSucceed { 80 | t.Fatal(err) 81 | } 82 | } 83 | 84 | func getOrMakeSignet(t *testing.T, tool tools.ToolLogic, recipient bool, signetID string) (*jess.Signet, error) { 85 | t.Helper() 86 | 87 | // check if signet already exists 88 | signet, err := testTrustStore.GetSignet(signetID, recipient) 89 | if err == nil { 90 | return signet, nil 91 | } 92 | 93 | // handle special cases 94 | if tool == nil { 95 | return nil, errors.New("bad parameters") 96 | } 97 | 98 | // create new signet 99 | newSignet := jess.NewSignetBase(tool.Definition()) 100 | newSignet.ID = signetID 101 | // generate signet and log time taken 102 | start := time.Now() 103 | err = tool.GenerateKey(newSignet) 104 | if err != nil { 105 | return nil, err 106 | } 107 | t.Logf("generated %s signet %s in %s", newSignet.Scheme, newSignet.ID, time.Since(start)) 108 | 109 | // store signet 110 | err = testTrustStore.StoreSignet(newSignet) 111 | if err != nil { 112 | return nil, err 113 | } 114 | 115 | // store recipient 116 | newRcpt, err := newSignet.AsRecipient() 117 | if err != nil { 118 | return nil, err 119 | } 120 | err = testTrustStore.StoreSignet(newRcpt) 121 | if err != nil { 122 | return nil, err 123 | } 124 | 125 | // return 126 | if recipient { 127 | return newRcpt, nil 128 | } 129 | return newSignet, nil 130 | } 131 | -------------------------------------------------------------------------------- /filesig/text_yaml.go: -------------------------------------------------------------------------------- 1 | package filesig 2 | 3 | // AddYAMLChecksum adds a checksum to a yaml file. 4 | func AddYAMLChecksum(data []byte, placement TextPlacement) ([]byte, error) { 5 | return AddTextFileChecksum(data, "#", placement) 6 | } 7 | 8 | // VerifyYAMLChecksum checks a checksum in a yaml file. 9 | func VerifyYAMLChecksum(data []byte) error { 10 | return VerifyTextFileChecksum(data, "#") 11 | } 12 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/safing/jess 2 | 3 | go 1.21.1 4 | 5 | toolchain go1.22.3 6 | 7 | require ( 8 | github.com/AlecAivazis/survey/v2 v2.3.7 9 | github.com/aead/ecdh v0.2.0 10 | github.com/mr-tron/base58 v1.2.0 11 | github.com/safing/structures v1.1.0 12 | github.com/satori/go.uuid v1.2.0 13 | github.com/spf13/cobra v1.8.1 14 | github.com/stretchr/testify v1.8.4 15 | github.com/tevino/abool v1.2.0 16 | github.com/tidwall/gjson v1.17.1 17 | github.com/tidwall/pretty v1.2.1 18 | github.com/tidwall/sjson v1.2.5 19 | github.com/zalando/go-keyring v0.2.5 20 | github.com/zeebo/blake3 v0.2.3 21 | golang.org/x/crypto v0.24.0 22 | golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 23 | ) 24 | 25 | require ( 26 | github.com/alessio/shellescape v1.4.2 // indirect 27 | github.com/danieljoos/wincred v1.2.1 // indirect 28 | github.com/davecgh/go-spew v1.1.1 // indirect 29 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 30 | github.com/ghodss/yaml v1.0.0 // indirect 31 | github.com/godbus/dbus/v5 v5.1.0 // indirect 32 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 33 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect 34 | github.com/klauspost/cpuid/v2 v2.2.8 // indirect 35 | github.com/kr/text v0.2.0 // indirect 36 | github.com/mattn/go-colorable v0.1.13 // indirect 37 | github.com/mattn/go-isatty v0.0.20 // indirect 38 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect 39 | github.com/pmezard/go-difflib v1.0.0 // indirect 40 | github.com/spf13/pflag v1.0.5 // indirect 41 | github.com/tidwall/match v1.1.1 // indirect 42 | github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect 43 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 44 | github.com/x448/float16 v0.8.4 // indirect 45 | golang.org/x/sys v0.21.0 // indirect 46 | golang.org/x/term v0.21.0 // indirect 47 | golang.org/x/text v0.16.0 // indirect 48 | gopkg.in/yaml.v2 v2.4.0 // indirect 49 | gopkg.in/yaml.v3 v3.0.1 // indirect 50 | ) 51 | -------------------------------------------------------------------------------- /hashtools/blake2.go: -------------------------------------------------------------------------------- 1 | package hashtools 2 | 3 | import ( 4 | "crypto" 5 | 6 | // Register BLAKE2 in Go's internal registry. 7 | _ "golang.org/x/crypto/blake2b" 8 | _ "golang.org/x/crypto/blake2s" 9 | 10 | "github.com/safing/jess/lhash" 11 | ) 12 | 13 | func init() { 14 | blake2bBase := &HashTool{ 15 | Comment: "RFC 7693, successor of SHA3 finalist, optimized for 64 bit software", 16 | Author: "Jean-Philippe Aumasson et al., 2013", 17 | } 18 | 19 | Register(blake2bBase.With(&HashTool{ 20 | Name: "BLAKE2s-256", 21 | NewHash: crypto.BLAKE2s_256.New, 22 | CryptoHashID: crypto.BLAKE2b_256, 23 | DigestSize: crypto.BLAKE2s_256.Size(), 24 | BlockSize: crypto.BLAKE2s_256.New().BlockSize(), 25 | SecurityLevel: 128, 26 | Comment: "RFC 7693, successor of SHA3 finalist, optimized for 8-32 bit software", 27 | labeledAlg: lhash.BLAKE2s_256, 28 | })) 29 | Register(blake2bBase.With(&HashTool{ 30 | Name: "BLAKE2b-256", 31 | NewHash: crypto.BLAKE2b_256.New, 32 | CryptoHashID: crypto.BLAKE2b_256, 33 | DigestSize: crypto.BLAKE2b_256.Size(), 34 | BlockSize: crypto.BLAKE2b_256.New().BlockSize(), 35 | SecurityLevel: 128, 36 | labeledAlg: lhash.BLAKE2b_256, 37 | })) 38 | Register(blake2bBase.With(&HashTool{ 39 | Name: "BLAKE2b-384", 40 | NewHash: crypto.BLAKE2b_384.New, 41 | CryptoHashID: crypto.BLAKE2b_384, 42 | DigestSize: crypto.BLAKE2b_384.Size(), 43 | BlockSize: crypto.BLAKE2b_384.New().BlockSize(), 44 | SecurityLevel: 192, 45 | labeledAlg: lhash.BLAKE2b_384, 46 | })) 47 | Register(blake2bBase.With(&HashTool{ 48 | Name: "BLAKE2b-512", 49 | NewHash: crypto.BLAKE2b_512.New, 50 | CryptoHashID: crypto.BLAKE2b_512, 51 | DigestSize: crypto.BLAKE2b_512.Size(), 52 | BlockSize: crypto.BLAKE2b_512.New().BlockSize(), 53 | SecurityLevel: 256, 54 | labeledAlg: lhash.BLAKE2b_512, 55 | })) 56 | } 57 | -------------------------------------------------------------------------------- /hashtools/blake3.go: -------------------------------------------------------------------------------- 1 | package hashtools 2 | 3 | import ( 4 | "hash" 5 | 6 | "github.com/zeebo/blake3" 7 | 8 | "github.com/safing/jess/lhash" 9 | ) 10 | 11 | func init() { 12 | Register(&HashTool{ 13 | Name: "BLAKE3", 14 | NewHash: newBlake3, 15 | DigestSize: newBlake3().Size(), 16 | BlockSize: newBlake3().BlockSize(), 17 | SecurityLevel: 128, 18 | Comment: "cryptographic hash function based on Bao and BLAKE2", 19 | Author: "Jean-Philippe Aumasson et al., 2020", 20 | labeledAlg: lhash.BLAKE3, 21 | }) 22 | } 23 | 24 | func newBlake3() hash.Hash { 25 | return blake3.New() 26 | } 27 | -------------------------------------------------------------------------------- /hashtools/hashtool.go: -------------------------------------------------------------------------------- 1 | package hashtools 2 | 3 | import ( 4 | "crypto" 5 | "hash" 6 | 7 | "github.com/safing/jess/lhash" 8 | ) 9 | 10 | // HashTool holds generic information about a hash tool. 11 | type HashTool struct { 12 | Name string 13 | 14 | NewHash func() hash.Hash 15 | CryptoHashID crypto.Hash 16 | 17 | DigestSize int // in bytes 18 | BlockSize int // in bytes 19 | SecurityLevel int // approx. attack complexity as 2^n 20 | 21 | Comment string 22 | Author string 23 | 24 | labeledAlg lhash.Algorithm 25 | } 26 | 27 | // New returns a new hash.Hash instance of the hash tool. 28 | func (ht *HashTool) New() hash.Hash { 29 | return ht.NewHash() 30 | } 31 | 32 | // With uses the original HashTool as a template for a new HashTool and returns the new HashTool. 33 | func (ht *HashTool) With(changes *HashTool) *HashTool { 34 | if changes.Name == "" { 35 | changes.Name = ht.Name 36 | } 37 | if changes.NewHash == nil { 38 | changes.NewHash = ht.NewHash 39 | } 40 | if changes.CryptoHashID == 0 { 41 | changes.CryptoHashID = ht.CryptoHashID 42 | } 43 | if changes.DigestSize == 0 { 44 | changes.DigestSize = ht.DigestSize 45 | } 46 | if changes.BlockSize == 0 { 47 | changes.BlockSize = ht.BlockSize 48 | } 49 | if changes.SecurityLevel == 0 { 50 | changes.SecurityLevel = ht.SecurityLevel 51 | } 52 | if changes.Comment == "" { 53 | changes.Comment = ht.Comment 54 | } 55 | if changes.Author == "" { 56 | changes.Author = ht.Author 57 | } 58 | if changes.labeledAlg == 0 { 59 | changes.labeledAlg = ht.labeledAlg 60 | } 61 | 62 | return changes 63 | } 64 | 65 | // LabeledHasher returns the corresponding labeled hashing algorithm. 66 | func (ht *HashTool) LabeledHasher() lhash.Algorithm { 67 | return ht.labeledAlg 68 | } 69 | -------------------------------------------------------------------------------- /hashtools/sha.go: -------------------------------------------------------------------------------- 1 | package hashtools 2 | 3 | import ( 4 | "crypto" 5 | // Register SHA2 in Go's internal registry. 6 | _ "crypto/sha256" 7 | _ "crypto/sha512" 8 | 9 | // Register SHA3 in Go's internal registry. 10 | _ "golang.org/x/crypto/sha3" 11 | 12 | "github.com/safing/jess/lhash" 13 | ) 14 | 15 | func init() { 16 | // SHA2 17 | sha2Base := &HashTool{ 18 | Comment: "FIPS 180-4", 19 | Author: "NSA, 2001", 20 | } 21 | Register(sha2Base.With(&HashTool{ 22 | Name: "SHA2-224", 23 | NewHash: crypto.SHA224.New, 24 | CryptoHashID: crypto.SHA224, 25 | DigestSize: crypto.SHA224.Size(), 26 | BlockSize: crypto.SHA224.New().BlockSize(), 27 | SecurityLevel: 112, 28 | Author: "NSA, 2004", 29 | labeledAlg: lhash.SHA2_224, 30 | })) 31 | Register(sha2Base.With(&HashTool{ 32 | Name: "SHA2-256", 33 | NewHash: crypto.SHA256.New, 34 | CryptoHashID: crypto.SHA256, 35 | DigestSize: crypto.SHA256.Size(), 36 | BlockSize: crypto.SHA256.New().BlockSize(), 37 | SecurityLevel: 128, 38 | labeledAlg: lhash.SHA2_256, 39 | })) 40 | Register(sha2Base.With(&HashTool{ 41 | Name: "SHA2-384", 42 | NewHash: crypto.SHA384.New, 43 | CryptoHashID: crypto.SHA384, 44 | DigestSize: crypto.SHA384.Size(), 45 | BlockSize: crypto.SHA384.New().BlockSize(), 46 | SecurityLevel: 192, 47 | labeledAlg: lhash.SHA2_384, 48 | })) 49 | Register(sha2Base.With(&HashTool{ 50 | Name: "SHA2-512", 51 | NewHash: crypto.SHA512.New, 52 | CryptoHashID: crypto.SHA512, 53 | DigestSize: crypto.SHA512.Size(), 54 | BlockSize: crypto.SHA512.New().BlockSize(), 55 | SecurityLevel: 256, 56 | labeledAlg: lhash.SHA2_512, 57 | })) 58 | Register(sha2Base.With(&HashTool{ 59 | Name: "SHA2-512-224", 60 | NewHash: crypto.SHA512_224.New, 61 | CryptoHashID: crypto.SHA512_224, 62 | DigestSize: crypto.SHA512_224.Size(), 63 | BlockSize: crypto.SHA512_224.New().BlockSize(), 64 | SecurityLevel: 112, 65 | labeledAlg: lhash.SHA2_512_224, 66 | })) 67 | Register(sha2Base.With(&HashTool{ 68 | Name: "SHA2-512-256", 69 | NewHash: crypto.SHA512_256.New, 70 | CryptoHashID: crypto.SHA512_256, 71 | DigestSize: crypto.SHA512_256.Size(), 72 | BlockSize: crypto.SHA512_256.New().BlockSize(), 73 | SecurityLevel: 128, 74 | labeledAlg: lhash.SHA2_512_256, 75 | })) 76 | 77 | // SHA3 78 | sha3Base := &HashTool{ 79 | Comment: "aka Keccak, FIPS-202, optimized for hardware", 80 | Author: "Guido Bertoni et al., 2015", 81 | } 82 | Register(sha3Base.With(&HashTool{ 83 | Name: "SHA3-224", 84 | NewHash: crypto.SHA3_224.New, 85 | CryptoHashID: crypto.SHA3_224, 86 | DigestSize: crypto.SHA3_224.Size(), 87 | BlockSize: crypto.SHA3_224.New().BlockSize(), 88 | SecurityLevel: 112, 89 | labeledAlg: lhash.SHA3_224, 90 | })) 91 | Register(sha3Base.With(&HashTool{ 92 | Name: "SHA3-256", 93 | NewHash: crypto.SHA3_256.New, 94 | CryptoHashID: crypto.SHA3_256, 95 | DigestSize: crypto.SHA3_256.Size(), 96 | BlockSize: crypto.SHA3_256.New().BlockSize(), 97 | SecurityLevel: 128, 98 | labeledAlg: lhash.SHA3_256, 99 | })) 100 | Register(sha3Base.With(&HashTool{ 101 | Name: "SHA3-384", 102 | NewHash: crypto.SHA3_384.New, 103 | CryptoHashID: crypto.SHA3_384, 104 | DigestSize: crypto.SHA3_384.Size(), 105 | BlockSize: crypto.SHA3_384.New().BlockSize(), 106 | SecurityLevel: 192, 107 | labeledAlg: lhash.SHA3_384, 108 | })) 109 | Register(sha3Base.With(&HashTool{ 110 | Name: "SHA3-512", 111 | NewHash: crypto.SHA3_512.New, 112 | CryptoHashID: crypto.SHA3_512, 113 | DigestSize: crypto.SHA3_512.Size(), 114 | BlockSize: crypto.SHA3_512.New().BlockSize(), 115 | SecurityLevel: 256, 116 | labeledAlg: lhash.SHA3_512, 117 | })) 118 | } 119 | -------------------------------------------------------------------------------- /hashtools/tools.go: -------------------------------------------------------------------------------- 1 | package hashtools 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "hash" 7 | "sort" 8 | ) 9 | 10 | var ( 11 | hashToolMap = make(map[string]*HashTool) 12 | hashToolList = sortableHashToolList{} 13 | 14 | // ErrNotFound is returned when a hash tool cannot be found. 15 | ErrNotFound = errors.New("does not exist") 16 | ) 17 | 18 | // Register registers a new HashTool. This function may only be called in init() functions. 19 | func Register(hashTool *HashTool) { 20 | hashToolMap[hashTool.Name] = hashTool 21 | hashToolList = append(hashToolList, hashTool) 22 | sort.Sort(hashToolList) 23 | } 24 | 25 | // Get returns the HashTool with the given name. 26 | func Get(name string) (*HashTool, error) { 27 | hashTool, ok := hashToolMap[name] 28 | if !ok { 29 | return nil, fmt.Errorf("tool %s %w", name, ErrNotFound) 30 | } 31 | return hashTool, nil 32 | } 33 | 34 | // New returns a new hash.Hash with the given name. 35 | func New(name string) (hash.Hash, error) { 36 | hashTool, err := Get(name) 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | return hashTool.New(), nil 42 | } 43 | 44 | // AsMap returns all HashTools in a map. The returned map must not be modified. 45 | func AsMap() map[string]*HashTool { 46 | return hashToolMap 47 | } 48 | 49 | // AsList returns all HashTools in a slice. The returned slice must not be modified. 50 | func AsList() []*HashTool { 51 | return hashToolList 52 | } 53 | 54 | type sortableHashToolList []*HashTool 55 | 56 | // Len implements sort.Interface. 57 | func (l sortableHashToolList) Len() int { return len(l) } 58 | 59 | // Swap implements sort.Interface. 60 | func (l sortableHashToolList) Swap(i, j int) { l[i], l[j] = l[j], l[i] } 61 | 62 | // Less implements sort.Interface. 63 | func (l sortableHashToolList) Less(i, j int) bool { return l[i].Name < l[j].Name } 64 | -------------------------------------------------------------------------------- /hashtools/tools_test.go: -------------------------------------------------------------------------------- 1 | package hashtools 2 | 3 | import ( 4 | "encoding/hex" 5 | "testing" 6 | ) 7 | 8 | func TestAll(t *testing.T) { 9 | t.Parallel() 10 | 11 | testData := []byte("The quick brown fox jumps over the lazy dog.") 12 | 13 | all := AsList() 14 | for _, hashTool := range all { 15 | // Test hash usage. 16 | hash, err := New(hashTool.Name) 17 | if err != nil { 18 | t.Fatalf("failed to get HashTool %s", hashTool.Name) 19 | } 20 | 21 | if hash.BlockSize() != hashTool.BlockSize { 22 | t.Errorf("hashTool %s is broken or reports invalid block size. Expected %d, got %d.", hashTool.Name, hashTool.BlockSize, hash.BlockSize()) 23 | } 24 | 25 | _, err = hash.Write(testData) 26 | if err != nil { 27 | t.Errorf("hashTool %s failed to write: %s", hashTool.Name, err) 28 | } 29 | 30 | sum := hash.Sum(nil) 31 | if len(sum) != hashTool.DigestSize { 32 | t.Errorf("hashTool %s is broken or reports invalid digest size. Expected %d, got %d.", hashTool.Name, hashTool.DigestSize, len(sum)) 33 | } 34 | 35 | // Check hash outputs. 36 | expectedOutputs, ok := testOutputs[hashTool.Name] 37 | if !ok { 38 | t.Errorf("no test outputs available for %s", hashTool.Name) 39 | continue 40 | } 41 | 42 | // Test empty string. 43 | hash.Reset() 44 | _, _ = hash.Write(testInputEmpty) 45 | hexSum := hex.EncodeToString(hash.Sum(nil)) 46 | if hexSum != expectedOutputs[0] { 47 | t.Errorf("hash tool %s: test empty: digest mismatch, expected %+v, got %+v", 48 | hashTool.Name, expectedOutputs[0], hexSum) 49 | } 50 | 51 | // Test fox string. 52 | hash.Reset() 53 | _, _ = hash.Write(testInputFox) 54 | hexSum = hex.EncodeToString(hash.Sum(nil)) 55 | if hexSum != expectedOutputs[1] { 56 | t.Errorf("hash tool %s: test empty: digest mismatch, expected %+v, got %+v", 57 | hashTool.Name, expectedOutputs[1], hexSum) 58 | } 59 | } 60 | } 61 | 62 | var ( 63 | testInputEmpty = []byte("") 64 | testInputFox = []byte("The quick brown fox jumps over the lazy dog.") 65 | ) 66 | 67 | var testOutputs = map[string][2]string{ 68 | "SHA2-224": { 69 | "d14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f", 70 | "619cba8e8e05826e9b8c519c0a5c68f4fb653e8a3d8aa04bb2c8cd4c", 71 | }, 72 | "SHA2-256": { 73 | "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", 74 | "ef537f25c895bfa782526529a9b63d97aa631564d5d789c2b765448c8635fb6c", 75 | }, 76 | "SHA2-384": { 77 | "38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b", 78 | "ed892481d8272ca6df370bf706e4d7bc1b5739fa2177aae6c50e946678718fc67a7af2819a021c2fc34e91bdb63409d7", 79 | }, 80 | "SHA2-512": { 81 | "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e", 82 | "91ea1245f20d46ae9a037a989f54f1f790f0a47607eeb8a14d12890cea77a1bbc6c7ed9cf205e67b7f2b8fd4c7dfd3a7a8617e45f3c463d481c7e586c39ac1ed", 83 | }, 84 | "SHA2-512-224": { 85 | "6ed0dd02806fa89e25de060c19d3ac86cabb87d6a0ddd05c333b84f4", 86 | "6d6a9279495ec4061769752e7ff9c68b6b0b3c5a281b7917ce0572de", 87 | }, 88 | "SHA2-512-256": { 89 | "c672b8d1ef56ed28ab87c3622c5114069bdd3ad7b8f9737498d0c01ecef0967a", 90 | "1546741840f8a492b959d9b8b2344b9b0eb51b004bba35c0aebaac86d45264c3", 91 | }, 92 | "SHA3-224": { 93 | "6b4e03423667dbb73b6e15454f0eb1abd4597f9a1b078e3f5b5a6bc7", 94 | "2d0708903833afabdd232a20201176e8b58c5be8a6fe74265ac54db0", 95 | }, 96 | "SHA3-256": { 97 | "a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a", 98 | "a80f839cd4f83f6c3dafc87feae470045e4eb0d366397d5c6ce34ba1739f734d", 99 | }, 100 | "SHA3-384": { 101 | "0c63a75b845e4f7d01107d852e4c2485c51a50aaaa94fc61995e71bbee983a2ac3713831264adb47fb6bd1e058d5f004", 102 | "1a34d81695b622df178bc74df7124fe12fac0f64ba5250b78b99c1273d4b080168e10652894ecad5f1f4d5b965437fb9", 103 | }, 104 | "SHA3-512": { 105 | "a69f73cca23a9ac5c8b567dc185a756e97c982164fe25859e0d1dcc1475c80a615b2123af1f5f94c11e3e9402c3ac558f500199d95b6d3e301758586281dcd26", 106 | "18f4f4bd419603f95538837003d9d254c26c23765565162247483f65c50303597bc9ce4d289f21d1c2f1f458828e33dc442100331b35e7eb031b5d38ba6460f8", 107 | }, 108 | "BLAKE2s-256": { 109 | "69217a3079908094e11121d042354a7c1f55b6482ca1a51e1b250dfd1ed0eef9", 110 | "95bca6e1b761dca1323505cc629949a0e03edf11633cc7935bd8b56f393afcf2", 111 | }, 112 | "BLAKE2b-256": { 113 | "0e5751c026e543b2e8ab2eb06099daa1d1e5df47778f7787faab45cdf12fe3a8", 114 | "69d7d3b0afba81826d27024c17f7f183659ed0812cf27b382eaef9fdc29b5712", 115 | }, 116 | "BLAKE2b-384": { 117 | "b32811423377f52d7862286ee1a72ee540524380fda1724a6f25d7978c6fd3244a6caf0498812673c5e05ef583825100", 118 | "16d65de1a3caf1c26247234c39af636284c7e19ca448c0de788272081410778852c94d9cef6b939968d4f872c7f78337", 119 | }, 120 | "BLAKE2b-512": { 121 | "786a02f742015903c6c6fd852552d272912f4740e15847618a86e217f71f5419d25e1031afee585313896444934eb04b903a685b1448b755d56f701afe9be2ce", 122 | "87af9dc4afe5651b7aa89124b905fd214bf17c79af58610db86a0fb1e0194622a4e9d8e395b352223a8183b0d421c0994b98286cbf8c68a495902e0fe6e2bda2", 123 | }, 124 | "BLAKE3": { 125 | "af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262", 126 | "4c9bd68d7f0baa2e167cef98295eb1ec99a3ec8f0656b33dbae943b387f31d5d", 127 | }, 128 | } 129 | -------------------------------------------------------------------------------- /helper.go: -------------------------------------------------------------------------------- 1 | package jess 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | 7 | "github.com/safing/jess/tools" 8 | ) 9 | 10 | var ( 11 | errNoSession = errors.New("helper is used outside of session") 12 | errNoKDF = errors.New("session has no key derivation tool") 13 | ) 14 | 15 | // Helper provides a basic interface for tools to access session properties and functionality. 16 | type Helper struct { 17 | session *Session 18 | info *tools.ToolInfo 19 | } 20 | 21 | // NewSessionKey returns a new session key in tool's specified length. 22 | func (h *Helper) NewSessionKey() ([]byte, error) { 23 | if h.session == nil { 24 | return nil, errNoSession 25 | } 26 | if h.session.kdf == nil { 27 | return nil, errNoKDF 28 | } 29 | 30 | if h.info.KeySize > 0 { 31 | return h.session.kdf.DeriveKey(h.info.KeySize) 32 | } 33 | return h.session.kdf.DeriveKey(h.session.DefaultSymmetricKeySize) 34 | } 35 | 36 | // FillNewSessionKey fills the given []byte slice with a new session key (or nonce). 37 | func (h *Helper) FillNewSessionKey(key []byte) error { 38 | if h.session == nil { 39 | return errNoSession 40 | } 41 | if h.session.kdf == nil { 42 | return errNoKDF 43 | } 44 | 45 | return h.session.kdf.DeriveKeyWriteTo(key) 46 | } 47 | 48 | // NewSessionNonce returns a new session nonce in tool's specified length. 49 | func (h *Helper) NewSessionNonce() ([]byte, error) { 50 | if h.session == nil { 51 | return nil, errNoSession 52 | } 53 | if h.session.kdf == nil { 54 | return nil, errNoKDF 55 | } 56 | 57 | if h.info.NonceSize > 0 { 58 | return h.session.kdf.DeriveKey(h.info.NonceSize) 59 | } 60 | return h.session.kdf.DeriveKey(h.session.DefaultSymmetricKeySize) 61 | } 62 | 63 | // Random returns the io.Reader for reading randomness. 64 | func (h *Helper) Random() io.Reader { 65 | return Random() 66 | } 67 | 68 | // RandomBytes returns the specified amount of random bytes in a []byte slice. 69 | func (h *Helper) RandomBytes(n int) ([]byte, error) { 70 | return RandomBytes(n) 71 | } 72 | 73 | // Burn gets rid of the given []byte slice(s). This is currently ineffective, see known issues in the project's README. 74 | func (h *Helper) Burn(data ...[]byte) { 75 | Burn(data...) 76 | } 77 | 78 | // DefaultSymmetricKeySize returns the default key size for this session. 79 | func (h *Helper) DefaultSymmetricKeySize() int { 80 | if h.session != nil && h.session.DefaultSymmetricKeySize > 0 { 81 | return h.session.DefaultSymmetricKeySize 82 | } 83 | return defaultSymmetricKeySize 84 | } 85 | 86 | // SecurityLevel returns the effective (ie. lowest) security level for this session. 87 | func (h *Helper) SecurityLevel() int { 88 | if h.session != nil && h.session.SecurityLevel > 0 { 89 | return h.session.SecurityLevel 90 | } 91 | return defaultSecurityLevel 92 | } 93 | 94 | // MaxSecurityLevel returns the (highest) security level for this session. 95 | func (h *Helper) MaxSecurityLevel() int { 96 | if h.session != nil && h.session.maxSecurityLevel > 0 { 97 | return h.session.maxSecurityLevel 98 | } 99 | return defaultSecurityLevel 100 | } 101 | 102 | // Burn gets rid of the given []byte slice(s). This is currently ineffective, see known issues in the project's README. 103 | func Burn(data ...[]byte) { 104 | for _, slice := range data { 105 | for i := 0; i < len(slice); i++ { 106 | slice[i] = 0xFF 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /letter-file.go: -------------------------------------------------------------------------------- 1 | package jess 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/safing/structures/container" 7 | "github.com/safing/structures/dsd" 8 | ) 9 | 10 | /* 11 | ### File Format Version 1 12 | 13 | - File Format Version: varint 14 | - Header: Letter without Data as byte block 15 | - Data: byte block 16 | */ 17 | 18 | // ErrIncompatibleFileFormatVersion is returned when an incompatible wire format is encountered. 19 | var ErrIncompatibleFileFormatVersion = errors.New("incompatible file format version") 20 | 21 | // ToFileFormat serializes the letter for storing it as a file. 22 | func (letter *Letter) ToFileFormat() (*container.Container, error) { 23 | c := container.New() 24 | 25 | // File Format Version: varint 26 | c.AppendNumber(1) 27 | 28 | // split header and data 29 | letterData := letter.Data 30 | letter.Data = nil 31 | 32 | // Header: Letter without Data as byte block 33 | headerData, err := dsd.DumpIndent(letter, dsd.JSON, "\t") 34 | if err != nil { 35 | return nil, err 36 | } 37 | // add newline for better raw viewability 38 | headerData = append(headerData, byte('\n')) 39 | // add header 40 | c.AppendAsBlock(headerData) 41 | 42 | // Data: byte block 43 | c.AppendAsBlock(letterData) 44 | 45 | // put back together 46 | letter.Data = letterData 47 | 48 | return c, nil 49 | } 50 | 51 | // LetterFromFileFormat parses a letter stored as a file. 52 | func LetterFromFileFormat(c *container.Container) (*Letter, error) { 53 | letter := &Letter{} 54 | 55 | // File Format Version: varint 56 | fileFormatVersion, err := c.GetNextN8() 57 | if err != nil { 58 | return nil, err 59 | } 60 | if fileFormatVersion != 1 { 61 | return nil, ErrIncompatibleFileFormatVersion 62 | } 63 | 64 | // Header: Letter without Data as byte block 65 | data, err := c.GetNextBlock() 66 | if err != nil { 67 | return nil, err 68 | } 69 | _, err = dsd.Load(data, letter) 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | // Data: byte block 75 | letterData, err := c.GetNextBlock() 76 | if err != nil { 77 | return nil, err 78 | } 79 | letter.Data = letterData 80 | 81 | return letter, nil 82 | } 83 | -------------------------------------------------------------------------------- /letter-wire.go: -------------------------------------------------------------------------------- 1 | package jess 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/safing/structures/container" 7 | ) 8 | 9 | /* 10 | ### Wire Format Version 1 11 | 12 | - Wire Format Version: varint 13 | - Flags: varint 14 | - 1: Setup Msg (includes Version and Tools) 15 | - 2: Sending Keys 16 | - 4: Apply Keys 17 | - Version: varint (if Setup Msg) 18 | - SuiteID: byte block (if Setup Msg) 19 | - Keys: 20 | - Amount: varint 21 | - IDs/Values: byte blocks 22 | - Nonce: byte block 23 | - Data: byte block 24 | - MAC: byte block 25 | */ 26 | 27 | // ErrIncompatibleWireFormatVersion is returned when an incompatible wire format is encountered. 28 | var ErrIncompatibleWireFormatVersion = errors.New("incompatible wire format version") 29 | 30 | // ToWire serializes to letter for sending it over a network connection. 31 | func (letter *Letter) ToWire() (*container.Container, error) { 32 | c := container.New() 33 | 34 | // Wire Format Version: varint 35 | c.AppendNumber(1) 36 | 37 | // Flags: varint 38 | // - 1: Setup Msg (includes Version and Tools) 39 | // - 2: Sending Keys 40 | // - 4: Apply Keys 41 | var flags uint64 42 | if letter.Version > 0 { 43 | flags |= 1 44 | } 45 | if len(letter.Keys) > 0 { 46 | flags |= 2 47 | } 48 | if letter.ApplyKeys { 49 | flags |= 4 50 | } 51 | c.AppendNumber(flags) 52 | 53 | if letter.Version > 0 { 54 | // Version: varint (if Setup Msg) 55 | c.AppendNumber(uint64(letter.Version)) 56 | 57 | // SuiteID: byte block (if Setup Msg) 58 | c.AppendAsBlock([]byte(letter.SuiteID)) 59 | } 60 | 61 | if len(letter.Keys) > 0 { 62 | // Keys: 63 | // - Amount: varint 64 | // - IDs/Values: byte blocks 65 | c.AppendInt(len(letter.Keys)) 66 | for _, seal := range letter.Keys { 67 | c.AppendAsBlock([]byte(seal.ID)) 68 | c.AppendAsBlock(seal.Value) 69 | } 70 | } 71 | 72 | // Nonce: byte block 73 | c.AppendAsBlock(letter.Nonce) 74 | 75 | // Data: byte block 76 | c.AppendAsBlock(letter.Data) 77 | 78 | // MAC: byte block 79 | c.AppendAsBlock(letter.Mac) 80 | 81 | // debugging: 82 | // fmt.Printf("%+v\n", c.CompileData()) 83 | 84 | return c, nil 85 | } 86 | 87 | // LetterFromWireData is a relay to LetterFromWire to quickly fix import issues of godep. 88 | // 89 | // Deprecated: Please use LetterFromWire with a fresh container directly. 90 | func LetterFromWireData(data []byte) (*Letter, error) { 91 | return LetterFromWire(container.New(data)) 92 | } 93 | 94 | // LetterFromWire parses a letter sent over a network connection. 95 | func LetterFromWire(c *container.Container) (*Letter, error) { 96 | letter := &Letter{} 97 | 98 | // Wire Format Version: varint 99 | wireFormatVersion, err := c.GetNextN8() 100 | if err != nil { 101 | return nil, err 102 | } 103 | if wireFormatVersion != 1 { 104 | return nil, ErrIncompatibleWireFormatVersion 105 | } 106 | 107 | // Flags: varint 108 | // - 1: Setup Msg (includes Version and Tools) 109 | // - 2: Sending Keys 110 | // - 4: Apply Keys 111 | var ( 112 | setupMsg bool 113 | sendingKeys bool 114 | ) 115 | flags, err := c.GetNextN64() 116 | if err != nil { 117 | return nil, err 118 | } 119 | if flags&1 > 0 { 120 | setupMsg = true 121 | } 122 | if flags&2 > 0 { 123 | sendingKeys = true 124 | } 125 | if flags&4 > 0 { 126 | letter.ApplyKeys = true 127 | } 128 | 129 | if setupMsg { 130 | // Version: varint (if Setup Msg) 131 | n, err := c.GetNextN8() 132 | if err != nil { 133 | return nil, err 134 | } 135 | letter.Version = n 136 | 137 | // SuiteID: byte block (if Setup Msg) 138 | suiteID, err := c.GetNextBlock() 139 | if err != nil { 140 | return nil, err 141 | } 142 | letter.SuiteID = string(suiteID) 143 | } 144 | 145 | if sendingKeys { 146 | // Keys: 147 | // - Amount: varint 148 | // - IDs/Values: byte blocks 149 | n, err := c.GetNextN8() 150 | if err != nil { 151 | return nil, err 152 | } 153 | letter.Keys = make([]*Seal, n) 154 | for i := 0; i < len(letter.Keys); i++ { 155 | signetID, err := c.GetNextBlock() 156 | if err != nil { 157 | return nil, err 158 | } 159 | sealValue, err := c.GetNextBlock() 160 | if err != nil { 161 | return nil, err 162 | } 163 | letter.Keys[i] = &Seal{ 164 | ID: string(signetID), 165 | Value: sealValue, 166 | } 167 | } 168 | } 169 | 170 | // Nonce: byte block 171 | letter.Nonce, err = c.GetNextBlock() 172 | if err != nil { 173 | return nil, err 174 | } 175 | 176 | // Data: byte block 177 | letter.Data, err = c.GetNextBlock() 178 | if err != nil { 179 | return nil, err 180 | } 181 | 182 | // MAC: byte block 183 | letter.Mac, err = c.GetNextBlock() 184 | if err != nil { 185 | return nil, err 186 | } 187 | 188 | return letter, nil 189 | } 190 | -------------------------------------------------------------------------------- /letter_test.go: -------------------------------------------------------------------------------- 1 | package jess 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "reflect" 8 | "testing" 9 | ) 10 | 11 | func TestSerialization(t *testing.T) { 12 | t.Parallel() 13 | 14 | subject := &Letter{ 15 | Version: 1, 16 | SuiteID: SuiteComplete, 17 | Keys: []*Seal{ 18 | {ID: "a"}, 19 | {ID: "b"}, 20 | {ID: "c"}, 21 | }, 22 | Nonce: []byte{1, 2, 3}, 23 | Data: []byte{4, 5, 6}, 24 | Mac: []byte{7, 8, 9}, 25 | ApplyKeys: true, 26 | } 27 | testSerialize(t, subject, true) 28 | 29 | subject.Version = 0 30 | subject.SuiteID = "" 31 | testSerialize(t, subject, true) 32 | 33 | subject.ApplyKeys = false 34 | testSerialize(t, subject, true) 35 | 36 | subject.Keys = nil 37 | testSerialize(t, subject, true) 38 | } 39 | 40 | func testSerialize(t *testing.T, letter *Letter, wireFormat bool) { //nolint:unparam 41 | t.Helper() 42 | 43 | // File Format 44 | 45 | fileData, err := letter.ToFileFormat() 46 | if err != nil { 47 | t.Error(err) 48 | return 49 | } 50 | 51 | letter2, err := LetterFromFileFormat(fileData) 52 | if err != nil { 53 | t.Error(err) 54 | return 55 | } 56 | 57 | err = letter.CheckEqual(letter2) 58 | if err != nil { 59 | t.Errorf("letters (file format) do not match: %s\n%+v\n%+v\n", err, jsonFormat(letter), jsonFormat(letter2)) 60 | return 61 | } 62 | 63 | // Wire Format 64 | 65 | if !wireFormat { 66 | return 67 | } 68 | 69 | wire, err := letter.ToWire() 70 | if err != nil { 71 | t.Error(err) 72 | return 73 | } 74 | 75 | letter3, err := LetterFromWire(wire) 76 | if err != nil { 77 | t.Error(err) 78 | return 79 | } 80 | 81 | err = letter.CheckEqual(letter3) 82 | if err != nil { 83 | t.Errorf("letters (wire format) do not match: %s\n%+v\n%+v\n", err, jsonFormat(letter), jsonFormat(letter3)) 84 | return 85 | } 86 | } 87 | 88 | func (letter *Letter) CheckEqual(other *Letter) error { 89 | letterValue := reflect.ValueOf(*letter) 90 | otherValue := reflect.ValueOf(*other) 91 | 92 | var ok bool 93 | numElements := letterValue.NumField() 94 | for i := 0; i < numElements; i++ { 95 | name := letterValue.Type().Field(i).Name 96 | switch name { 97 | case "Data": // TODO: this required special handling in the past, leave it here for now. 98 | ok = bytes.Equal(letter.Data, other.Data) 99 | default: 100 | ok = reflect.DeepEqual(letterValue.Field(i).Interface(), otherValue.Field(i).Interface()) 101 | } 102 | 103 | if !ok { 104 | return fmt.Errorf("field %s mismatches", name) 105 | } 106 | } 107 | 108 | return nil 109 | } 110 | 111 | func jsonFormat(v interface{}) string { 112 | formatted, err := json.MarshalIndent(v, "", " ") 113 | if err != nil { 114 | return fmt.Sprintf("", err) 115 | } 116 | return string(formatted) 117 | } 118 | -------------------------------------------------------------------------------- /lhash/algs.go: -------------------------------------------------------------------------------- 1 | // Package lhash provides integrated labeled hashes. 2 | // 3 | //nolint:gci 4 | package lhash 5 | 6 | import ( 7 | "crypto" 8 | "hash" 9 | "io" 10 | 11 | // Register SHA2 in Go's internal registry. 12 | _ "crypto/sha256" 13 | _ "crypto/sha512" 14 | 15 | // Register SHA3 in Go's internal registry. 16 | _ "golang.org/x/crypto/sha3" 17 | 18 | // Register BLAKE2 in Go's internal registry. 19 | _ "golang.org/x/crypto/blake2b" 20 | _ "golang.org/x/crypto/blake2s" 21 | 22 | "github.com/zeebo/blake3" 23 | ) 24 | 25 | // Algorithm is an identifier for a hash function. 26 | type Algorithm uint 27 | 28 | //nolint:golint,stylecheck // names are really the best this way 29 | const ( 30 | SHA2_224 Algorithm = 8 31 | SHA2_256 Algorithm = 9 32 | SHA2_384 Algorithm = 10 33 | SHA2_512 Algorithm = 11 34 | SHA2_512_224 Algorithm = 12 35 | SHA2_512_256 Algorithm = 13 36 | 37 | SHA3_224 Algorithm = 16 38 | SHA3_256 Algorithm = 17 39 | SHA3_384 Algorithm = 18 40 | SHA3_512 Algorithm = 19 41 | 42 | BLAKE2s_256 Algorithm = 24 43 | BLAKE2b_256 Algorithm = 25 44 | BLAKE2b_384 Algorithm = 26 45 | BLAKE2b_512 Algorithm = 27 46 | 47 | BLAKE3 Algorithm = 32 48 | ) 49 | 50 | func (a Algorithm) new() hash.Hash { 51 | switch a { 52 | 53 | // SHA2 54 | case SHA2_224: 55 | return crypto.SHA224.New() 56 | case SHA2_256: 57 | return crypto.SHA256.New() 58 | case SHA2_384: 59 | return crypto.SHA384.New() 60 | case SHA2_512: 61 | return crypto.SHA512.New() 62 | case SHA2_512_224: 63 | return crypto.SHA512_224.New() 64 | case SHA2_512_256: 65 | return crypto.SHA512_256.New() 66 | 67 | // SHA3 68 | case SHA3_224: 69 | return crypto.SHA3_224.New() 70 | case SHA3_256: 71 | return crypto.SHA3_256.New() 72 | case SHA3_384: 73 | return crypto.SHA3_384.New() 74 | case SHA3_512: 75 | return crypto.SHA3_512.New() 76 | 77 | // BLAKE2 78 | case BLAKE2s_256: 79 | return crypto.BLAKE2s_256.New() 80 | case BLAKE2b_256: 81 | return crypto.BLAKE2b_256.New() 82 | case BLAKE2b_384: 83 | return crypto.BLAKE2b_384.New() 84 | case BLAKE2b_512: 85 | return crypto.BLAKE2b_512.New() 86 | 87 | // BLAKE3 88 | case BLAKE3: 89 | return blake3.New() 90 | 91 | default: 92 | return nil 93 | } 94 | } 95 | 96 | func (a Algorithm) String() string { 97 | switch a { 98 | 99 | // SHA2 100 | case SHA2_224: 101 | return "SHA2_224" 102 | case SHA2_256: 103 | return "SHA2_256" 104 | case SHA2_384: 105 | return "SHA2_384" 106 | case SHA2_512: 107 | return "SHA2_512" 108 | case SHA2_512_224: 109 | return "SHA2_512_224" 110 | case SHA2_512_256: 111 | return "SHA2_512_256" 112 | 113 | // SHA3 114 | case SHA3_224: 115 | return "SHA3_224" 116 | case SHA3_256: 117 | return "SHA3_256" 118 | case SHA3_384: 119 | return "SHA3_384" 120 | case SHA3_512: 121 | return "SHA3_512" 122 | 123 | // BLAKE2 124 | case BLAKE2s_256: 125 | return "BLAKE2s_256" 126 | case BLAKE2b_256: 127 | return "BLAKE2b_256" 128 | case BLAKE2b_384: 129 | return "BLAKE2b_384" 130 | case BLAKE2b_512: 131 | return "BLAKE2b_512" 132 | 133 | // BLAKE3 134 | case BLAKE3: 135 | return "BLAKE3" 136 | 137 | default: 138 | return "unknown" 139 | } 140 | } 141 | 142 | // RawHasher returns a new raw hasher of the algorithm. 143 | func (a Algorithm) RawHasher() hash.Hash { 144 | return a.new() 145 | } 146 | 147 | // Digest creates a new labeled hash and digests the given data. 148 | func (a Algorithm) Digest(data []byte) *LabeledHash { 149 | return Digest(a, data) 150 | } 151 | 152 | // DigestFile creates a new labeled hash and digests the given file. 153 | func (a Algorithm) DigestFile(pathToFile string) (*LabeledHash, error) { 154 | return DigestFile(a, pathToFile) 155 | } 156 | 157 | // DigestFromReader creates a new labeled hash and digests from the given reader. 158 | func (a Algorithm) DigestFromReader(reader io.Reader) (*LabeledHash, error) { 159 | return DigestFromReader(a, reader) 160 | } 161 | -------------------------------------------------------------------------------- /pack: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | baseDir="$( cd "$(dirname "$0")" && pwd )" 4 | cd "$baseDir" 5 | 6 | COL_OFF="\033[00m" 7 | COL_BOLD="\033[01;01m" 8 | COL_RED="\033[31m" 9 | 10 | destDirPart1="dist" 11 | destDirPart2="jess" 12 | 13 | function prep { 14 | # output 15 | output="cmd/jess" 16 | # get version 17 | version=$(grep "info.Set" cmd/main.go | cut -d'"' -f4) 18 | # build versioned file name 19 | filename="jess_v${version//./-}" 20 | # platform 21 | platform="${GOOS}_${GOARCH}" 22 | if [[ $GOOS == "windows" ]]; then 23 | filename="${filename}.exe" 24 | output="${output}.exe" 25 | fi 26 | # build destination path 27 | destPath=${destDirPart1}/${platform}/${destDirPart2}/$filename 28 | } 29 | 30 | function check { 31 | prep 32 | 33 | # check if file exists 34 | if [[ -f $destPath ]]; then 35 | echo "[jess] $platform $version already built" 36 | else 37 | echo -e "[jess] ${COL_BOLD}$platform $version${COL_OFF}" 38 | fi 39 | } 40 | 41 | function build { 42 | prep 43 | 44 | # check if file exists 45 | if [[ -f $destPath ]]; then 46 | echo "[jess] $platform already built in version $version, skipping..." 47 | return 48 | fi 49 | 50 | # build 51 | ./cmd/build 52 | if [[ $? -ne 0 ]]; then 53 | echo -e "\n${COL_BOLD}[jess] $platform: ${COL_RED}BUILD FAILED.${COL_OFF}" 54 | exit 1 55 | fi 56 | mkdir -p $(dirname $destPath) 57 | cp $output $destPath 58 | echo -e "\n${COL_BOLD}[jess] $platform: successfully built.${COL_OFF}" 59 | } 60 | 61 | function check_all { 62 | GOOS=linux GOARCH=amd64 check 63 | GOOS=windows GOARCH=amd64 check 64 | GOOS=darwin GOARCH=amd64 check 65 | GOOS=linux GOARCH=arm64 check 66 | GOOS=windows GOARCH=arm64 check 67 | GOOS=darwin GOARCH=arm64 check 68 | } 69 | 70 | function build_all { 71 | GOOS=linux GOARCH=amd64 build 72 | GOOS=windows GOARCH=amd64 build 73 | GOOS=darwin GOARCH=amd64 build 74 | GOOS=linux GOARCH=arm64 build 75 | GOOS=windows GOARCH=arm64 build 76 | GOOS=darwin GOARCH=arm64 build 77 | } 78 | 79 | function build_os { 80 | # build only for current OS 81 | # set for script 82 | GOOS=$(go env GOOS) 83 | # architectures 84 | GOARCH=amd64 build 85 | } 86 | 87 | case $1 in 88 | "check" ) 89 | check_all 90 | ;; 91 | "build" ) 92 | build_all 93 | ;; 94 | "build-os" ) 95 | build_os 96 | ;; 97 | * ) 98 | echo "" 99 | echo "build list:" 100 | echo "" 101 | check_all 102 | echo "" 103 | read -p "press [Enter] to start building" x 104 | echo "" 105 | build_all 106 | echo "" 107 | echo "finished building." 108 | echo "" 109 | ;; 110 | esac 111 | -------------------------------------------------------------------------------- /password.go: -------------------------------------------------------------------------------- 1 | package jess 2 | 3 | import ( 4 | "math" 5 | "strings" 6 | ) 7 | 8 | var ( 9 | // ASCII printable characters (character codes 32-127). 10 | passwordCharSets = []string{ 11 | "abcdefghijklmnopqrstuvwxyz", 12 | "ABCDEFGHIJKLMNOPQRSTUVWXYZ", 13 | "0123456789", 14 | "- .,_", // more common special characters, especially with passwords using words 15 | "!\"#$%&'()*+/:;<=>?@[\\]^`{|}~", 16 | } 17 | 18 | // extended ASCII codes (character code 128-255) 19 | // assume pool size of 32 (a quarter), as not all of them are common / easily accessible on every keyboard. 20 | passwordExtraPoolSize = 32 21 | 22 | createPasswordCallback func(signet *Signet, minSecurityLevel int) error 23 | getPasswordCallback func(signet *Signet) error 24 | ) 25 | 26 | // SetPasswordCallbacks sets callbacks that are used to let the user enter passwords. 27 | func SetPasswordCallbacks( 28 | createPassword func(signet *Signet, minSecurityLevel int) error, 29 | getPassword func(signet *Signet) error, 30 | ) { 31 | if createPasswordCallback == nil { 32 | createPasswordCallback = createPassword 33 | } 34 | if getPasswordCallback == nil { 35 | getPasswordCallback = getPassword 36 | } 37 | } 38 | 39 | // CalculatePasswordSecurityLevel calculates the security level of the given password and iterations of the pbkdf algorithm. 40 | func CalculatePasswordSecurityLevel(password string, iterations int) int { 41 | // TODO: this calculation is pretty conservative and errs on the safe side 42 | // maybe soften this up a litte, but couldn't find any scientific foundation for that 43 | 44 | charactersFound := 0 45 | distinctCharactersFound := 0 46 | characterPoolSize := 0 47 | 48 | // loop all character sets 49 | for _, charSet := range passwordCharSets { 50 | foundInCharSet := false 51 | 52 | // loop through every character in the character set 53 | for _, char := range charSet { 54 | // count occurrences in password 55 | cnt := countRuneInString(password, char) 56 | // disqualify if a single character is 1/4 of the password 57 | if cnt*4 >= len(password) { 58 | return -1 59 | } 60 | // we found something! 61 | if cnt > 0 { 62 | charactersFound += cnt 63 | distinctCharactersFound++ 64 | foundInCharSet = true 65 | } 66 | } 67 | 68 | // if we found anything in this char set, add the it's length to the total pool 69 | if foundInCharSet { 70 | characterPoolSize += len(charSet) 71 | } 72 | } 73 | 74 | // disqualify if characters are repeated 4 or more times, on average 75 | if distinctCharactersFound*4 <= len(password) { 76 | return -1 77 | } 78 | 79 | // check if there are some extra characters 80 | if charactersFound < len(password) { 81 | // add the extra pool size 82 | characterPoolSize += passwordExtraPoolSize 83 | } 84 | 85 | possibleCombinationsWithPoolSize := math.Pow(float64(characterPoolSize), float64(len(password))) 86 | entropy := math.Log2(possibleCombinationsWithPoolSize) 87 | avgNumberOfGuesses := math.Pow(2, entropy-1) 88 | avgGuessingOperations := avgNumberOfGuesses * float64(iterations) 89 | securityLevel := math.Log2(avgGuessingOperations) 90 | 91 | return int(securityLevel) // always round down 92 | } 93 | 94 | func countRuneInString(s string, r rune) (n int) { 95 | for { 96 | i := strings.IndexRune(s, r) 97 | if i < 0 { 98 | return 99 | } 100 | n++ 101 | s = s[i+1:] 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /password_test.go: -------------------------------------------------------------------------------- 1 | package jess 2 | 3 | import "testing" 4 | 5 | func init() { 6 | SetPasswordCallbacks( 7 | func(signet *Signet, minSecurityLevel int) error { 8 | return getTestPassword(signet) 9 | }, 10 | getTestPassword, 11 | ) 12 | } 13 | 14 | func getTestPassword(signet *Signet) error { 15 | pwSignet, err := testTrustStore.GetSignet(signet.ID, false) 16 | if err != nil { 17 | return err 18 | } 19 | signet.Key = pwSignet.Key 20 | return nil 21 | } 22 | 23 | func TestCalculatePasswordSecurityLevel(t *testing.T) { 24 | t.Parallel() 25 | 26 | // basic weak 27 | testPWSL(t, "asdf", -1) 28 | testPWSL(t, "asdfasdf", -1) 29 | testPWSL(t, "asdfasdxxxx", -1) 30 | testPWSL(t, "asdfasdfasdf", -1) 31 | testPWSL(t, "asdfasdfasdf", -1) 32 | testPWSL(t, "WgEKCp8c8{bPrG{Zo(Ms97pxaaaaaaaa", -1) 33 | testPWSL(t, "aaaaaaaaAAAAAAAA00000000********", -1) 34 | 35 | // chars only 36 | testPWSL(t, "AVWHBwmF", 64) 37 | testPWSL(t, "AVWHBwmFGt", 76) 38 | testPWSL(t, "AVWHBwmFGtLM", 87) 39 | testPWSL(t, "AVWHBwmFGtLMGh", 98) 40 | testPWSL(t, "AVWHBwmFGtLMGhYf", 110) 41 | testPWSL(t, "AVWHBwmFGtLMGhYfPkcyawfmZXRTQdxs", 201) 42 | 43 | // with number 44 | testPWSL(t, "AVWHBwm1", 66) 45 | testPWSL(t, "AVWHBwmFG1", 78) 46 | testPWSL(t, "AVWHBwmFGtL1", 90) 47 | testPWSL(t, "AVWHBwmFGtLMG1", 102) 48 | testPWSL(t, "AVWHBwmFGtLMGhY1", 114) 49 | testPWSL(t, "AVWHBwmFGtLMGhYfPkcyawfmZXRTQdx1", 209) 50 | 51 | // with number and special 52 | testPWSL(t, "AVWHBw1_", 67) 53 | testPWSL(t, "AVWHBwmF1_", 79) 54 | testPWSL(t, "AVWHBwmFGt1_", 91) 55 | testPWSL(t, "AVWHBwmFGtLM1_", 103) 56 | testPWSL(t, "AVWHBwmFGtLMGh1_", 116) 57 | testPWSL(t, "AVWHBwmFGtLMGhYfPkcyawfmZXRTQd1_", 213) 58 | 59 | // with number and more special 60 | testPWSL(t, "AVWHBw1*", 70) 61 | testPWSL(t, "AVWHBwmF1*", 83) 62 | testPWSL(t, "AVWHBwmFGt1*", 96) 63 | testPWSL(t, "AVWHBwmFGtLM1*", 109) 64 | testPWSL(t, "AVWHBwmFGtLMGh1*", 122) 65 | testPWSL(t, "AVWHBwmFGtLMGhYfPkcyawfmZXRTQd1*", 226) 66 | 67 | // created, strong 68 | 69 | // "Schneier scheme" 70 | // source: https://www.schneier.com/blog/archives/2014/03/choosing_secure_1.html 71 | testPWSL(t, "WIw7,mstmsritt...", 122) 72 | testPWSL(t, "Wow...doestcst", 100) 73 | testPWSL(t, "Ltime@go-inag~faaa!", 140) 74 | testPWSL(t, "uTVM,TPw55:utvm,tpwstillsecure", 216) 75 | 76 | // generated, strong 77 | testPWSL(t, "YebGPQuuoxQwyeJMvEWACTLexUUxVBFdHYqqUybBUNfBttCvWQxDdDCdYfgMPCQp", 383) 78 | testPWSL(t, "dpPyXmXpbECn6LWuQDJaitTTJguGfRTqNUxWfoHnBKDHvRhjR2WiQ7iDcuRJNnEd", 400) 79 | testPWSL(t, "WgEKCp8c8{bPrG{Zo(Ms97pKt3EsR9ycz4R=kMjPp^Uafqxsd2ZTFtkfvnoueKJz", 434) 80 | testPWSL(t, "galena-fighter-festival", 132) 81 | testPWSL(t, "impotent-drug-dropout-damage", 157) 82 | testPWSL(t, "artless-newswire-rill-belgium-marplot", 202) 83 | testPWSL(t, "forbade-momenta-spook-sure-devilish-wobbly", 227) 84 | } 85 | 86 | func testPWSL(t *testing.T, password string, expectedSecurityLevel int) { 87 | t.Helper() 88 | 89 | securityLevel := CalculatePasswordSecurityLevel(password, 1<<20) 90 | 91 | if securityLevel < expectedSecurityLevel { 92 | t.Errorf("password %s (%di): %d - expected at least %d", password, 1<<20, securityLevel, expectedSecurityLevel) 93 | } else { 94 | t.Logf("password %s (%di): %d", password, 1<<20, securityLevel) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /random.go: -------------------------------------------------------------------------------- 1 | package jess 2 | 3 | import ( 4 | "crypto/rand" 5 | "io" 6 | 7 | "github.com/tevino/abool" 8 | ) 9 | 10 | var ( 11 | customRandReader io.Reader 12 | customRandReaderFlag = abool.NewBool(false) 13 | ) 14 | 15 | // Random returns the io.Reader for reading randomness. By default, it uses crypto/rand.Reader. 16 | func Random() io.Reader { 17 | if customRandReaderFlag.IsSet() { 18 | return customRandReader 19 | } 20 | return rand.Reader 21 | } 22 | 23 | // RandomBytes returns the specified amount of random bytes in a []byte slice. By default, it uses crypto/rand.Reader. 24 | func RandomBytes(n int) ([]byte, error) { 25 | rBytes := make([]byte, n) 26 | 27 | bytesRead, err := Random().Read(rBytes) 28 | if err != nil { 29 | return nil, err 30 | } 31 | if bytesRead != n { 32 | return nil, ErrInsufficientRandom 33 | } 34 | 35 | return rBytes, nil 36 | } 37 | 38 | // SetCustomRNG sets a custom RNG to be used with jess. 39 | func SetCustomRNG(randReader io.Reader) { 40 | if !customRandReaderFlag.IsSet() { 41 | customRandReader = randReader 42 | customRandReaderFlag.Set() 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /requirements.go: -------------------------------------------------------------------------------- 1 | package jess 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // Security requirements of a letter. 9 | const ( 10 | Confidentiality uint8 = iota 11 | Integrity 12 | RecipientAuthentication 13 | SenderAuthentication 14 | ) 15 | 16 | // Requirements describe security properties. 17 | type Requirements struct { 18 | all []uint8 19 | } 20 | 21 | // newEmptyRequirements returns an empty requirements instance. 22 | func newEmptyRequirements() *Requirements { 23 | return &Requirements{} 24 | } 25 | 26 | // NewRequirements returns an attribute instance with all requirements. 27 | func NewRequirements() *Requirements { 28 | return &Requirements{ 29 | all: []uint8{ 30 | Confidentiality, 31 | Integrity, 32 | RecipientAuthentication, 33 | SenderAuthentication, 34 | }, 35 | } 36 | } 37 | 38 | // Empty returns whether the requirements are empty. 39 | func (requirements *Requirements) Empty() bool { 40 | return len(requirements.all) == 0 41 | } 42 | 43 | // Has returns whether the requirements contain the given attribute. 44 | func (requirements *Requirements) Has(attribute uint8) bool { 45 | for _, attr := range requirements.all { 46 | if attr == attribute { 47 | return true 48 | } 49 | } 50 | return false 51 | } 52 | 53 | // Add adds an attribute. 54 | func (requirements *Requirements) Add(attribute uint8) *Requirements { 55 | if !requirements.Has(attribute) { 56 | requirements.all = append(requirements.all, attribute) 57 | } 58 | return requirements 59 | } 60 | 61 | // Remove removes an attribute. 62 | func (requirements *Requirements) Remove(attribute uint8) *Requirements { 63 | for i, attr := range requirements.all { 64 | if attr == attribute { 65 | requirements.all = append(requirements.all[:i], requirements.all[i+1:]...) 66 | return requirements 67 | } 68 | } 69 | return requirements 70 | } 71 | 72 | // CheckComplianceTo checks if the requirements are compliant to the given required requirements. 73 | func (requirements *Requirements) CheckComplianceTo(requirement *Requirements) error { 74 | var missing *Requirements 75 | for _, attr := range requirement.all { 76 | if !requirements.Has(attr) { 77 | if missing == nil { 78 | missing = newEmptyRequirements() 79 | } 80 | missing.Add(attr) 81 | } 82 | } 83 | if missing != nil { 84 | return fmt.Errorf("missing security requirements: %s", missing.String()) 85 | } 86 | return nil 87 | } 88 | 89 | // String returns a string representation of the requirements. 90 | func (requirements *Requirements) String() string { 91 | var names []string 92 | for _, attr := range requirements.all { 93 | switch attr { 94 | case Confidentiality: 95 | names = append(names, "Confidentiality") 96 | case Integrity: 97 | names = append(names, "Integrity") 98 | case RecipientAuthentication: 99 | names = append(names, "RecipientAuthentication") 100 | case SenderAuthentication: 101 | names = append(names, "SenderAuthentication") 102 | } 103 | } 104 | return strings.Join(names, ", ") 105 | } 106 | 107 | // ShortString returns a short string representation of the requirements. 108 | func (requirements *Requirements) ShortString() string { 109 | var s string 110 | if requirements.Has(Confidentiality) { 111 | s += "C" 112 | } 113 | if requirements.Has(Integrity) { 114 | s += "I" 115 | } 116 | if requirements.Has(RecipientAuthentication) { 117 | s += "R" 118 | } 119 | if requirements.Has(SenderAuthentication) { 120 | s += "S" 121 | } 122 | return s 123 | } 124 | 125 | // SerializeToNoSpec returns the requirements as a negated "No" string. 126 | func (requirements *Requirements) SerializeToNoSpec() string { 127 | var s string 128 | if !requirements.Has(Confidentiality) { 129 | s += "C" 130 | } 131 | if !requirements.Has(Integrity) { 132 | s += "I" 133 | } 134 | if !requirements.Has(RecipientAuthentication) { 135 | s += "R" 136 | } 137 | if !requirements.Has(SenderAuthentication) { 138 | s += "S" 139 | } 140 | return s 141 | } 142 | 143 | // ParseRequirementsFromNoSpec parses the requirements from a negated "No" string. 144 | func ParseRequirementsFromNoSpec(no string) (*Requirements, error) { 145 | requirements := NewRequirements() 146 | for _, id := range no { 147 | switch id { 148 | case 'C': 149 | requirements.Remove(Confidentiality) 150 | case 'I': 151 | requirements.Remove(Integrity) 152 | case 'R': 153 | requirements.Remove(RecipientAuthentication) 154 | case 'S': 155 | requirements.Remove(SenderAuthentication) 156 | default: 157 | return nil, fmt.Errorf("unknown attribute identifier: %c", id) 158 | } 159 | } 160 | return requirements, nil 161 | } 162 | -------------------------------------------------------------------------------- /requirements_test.go: -------------------------------------------------------------------------------- 1 | package jess 2 | 3 | import "testing" 4 | 5 | func checkNoSpec(t *testing.T, a *Requirements, expectedNoSpec string) { 6 | t.Helper() 7 | 8 | noSpec := a.SerializeToNoSpec() 9 | if noSpec != expectedNoSpec { 10 | t.Errorf(`unexpected no spec "%s", expected "%s"`, noSpec, expectedNoSpec) 11 | } 12 | } 13 | 14 | func TestRequirements(t *testing.T) { 15 | t.Parallel() 16 | 17 | a := NewRequirements() 18 | checkNoSpec(t, a, "") 19 | 20 | a.Remove(SenderAuthentication) 21 | checkNoSpec(t, a, "S") 22 | 23 | a.Remove(RecipientAuthentication) 24 | checkNoSpec(t, a, "RS") 25 | 26 | a.Remove(Integrity) 27 | checkNoSpec(t, a, "IRS") 28 | 29 | a.Remove(Confidentiality) 30 | checkNoSpec(t, a, "CIRS") 31 | 32 | a.Add(SenderAuthentication) 33 | checkNoSpec(t, a, "CIR") 34 | 35 | a.Add(RecipientAuthentication) 36 | checkNoSpec(t, a, "CI") 37 | 38 | a.Add(Integrity) 39 | checkNoSpec(t, a, "C") 40 | 41 | a.Add(Confidentiality) 42 | checkNoSpec(t, a, "") 43 | } 44 | -------------------------------------------------------------------------------- /session-wire.go: -------------------------------------------------------------------------------- 1 | package jess 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/safing/jess/tools" 8 | ) 9 | 10 | const ( 11 | wireStateInit uint8 = iota 12 | wireStateIdle 13 | wireStateSendKey 14 | wireStateAwaitKey 15 | wireStateSendApply 16 | wireStatsAwaitApply 17 | ) 18 | 19 | var ( 20 | // Re-exchange keys every x messages. 21 | // At 10_000_000 msgs with 1500 bytes per msg, this would result in 22 | // re-exchanging keys every 15 GB. 23 | wireReKeyAfterMsgs uint64 = 10_000_000 24 | 25 | requiredWireSessionRequirements = NewRequirements().Remove(SenderAuthentication) 26 | ) 27 | 28 | // WireSession holds session information specific to communication over a network connection. 29 | type WireSession struct { //nolint:maligned // TODO 30 | session *Session 31 | 32 | server bool 33 | msgNo uint64 34 | lastReKeyAtMsgNo uint64 35 | 36 | sendKeyCarryover []byte 37 | recvKeyCarryover []byte 38 | 39 | // key mgmt state 40 | eKXSignets []*kxPair 41 | eKESignets []*kePair 42 | handshakeState uint8 43 | newKeyMaterial [][]byte 44 | } 45 | 46 | // kxPair is key exchange pair. 47 | type kxPair struct { 48 | tool tools.ToolLogic 49 | signet *Signet 50 | peer *Signet 51 | } 52 | 53 | // kePair is key encapsulation "pair". 54 | type kePair struct { 55 | tool tools.ToolLogic 56 | signet *Signet 57 | seal *Seal 58 | } 59 | 60 | // initWireSession is called after newSession() to make a wire session from a regular one. 61 | func (s *Session) initWireSession() error { 62 | // check required requirements 63 | err := s.toolRequirements.CheckComplianceTo(requiredWireSessionRequirements) 64 | if err != nil { 65 | return err 66 | } 67 | 68 | // check for currently unsupported features 69 | for _, tool := range s.all { 70 | switch tool.Info().Purpose { 71 | case tools.PurposePassDerivation, 72 | tools.PurposeSigning: 73 | return fmt.Errorf("wire sessions currently do not support %s", tool.Info().Name) 74 | } 75 | } 76 | 77 | // check for static pre shared keys 78 | err = s.envelope.LoopSecrets(SignetSchemeKey, func(signet *Signet) error { 79 | return errors.New("wire sessions currently do not support pre-shared keys") 80 | }) 81 | if err != nil { 82 | return err 83 | } 84 | 85 | s.wire = &WireSession{ 86 | session: s, 87 | } 88 | 89 | return nil 90 | } 91 | 92 | // Server marks a wire session as being in the role of the server, rather than the client. 93 | func (s *Session) Server() { 94 | if s.wire != nil { 95 | s.wire.server = true 96 | } 97 | } 98 | 99 | // reKeyNeeded returns whether rekeying is needed. 100 | func (w *WireSession) reKeyNeeded() bool { 101 | return w.msgNo-w.lastReKeyAtMsgNo > wireReKeyAfterMsgs 102 | } 103 | -------------------------------------------------------------------------------- /suite.go: -------------------------------------------------------------------------------- 1 | package jess 2 | 3 | // Suite status options. 4 | const ( 5 | SuiteStatusDeprecated uint8 = 0 6 | SuiteStatusPermitted uint8 = 1 7 | SuiteStatusRecommended uint8 = 2 8 | ) 9 | 10 | // Suite describes a cipher suite - a set of algorithms and the attributes they provide. 11 | type Suite struct { 12 | ID string 13 | Tools []string 14 | Provides *Requirements 15 | SecurityLevel int 16 | Status uint8 17 | } 18 | -------------------------------------------------------------------------------- /suites.go: -------------------------------------------------------------------------------- 1 | package jess 2 | 3 | // Currently Recommended Suites. 4 | var ( 5 | // SuiteKey is a cipher suite for encryption with a key. 6 | SuiteKey = SuiteKeyV1 7 | // SuitePassword is a cipher suite for encryption with a password. 8 | SuitePassword = SuitePasswordV1 9 | // SuiteRcptOnly is a cipher suite for encrypting for someone, but without verifying the sender/source. 10 | SuiteRcptOnly = SuiteRcptOnlyV1 11 | // SuiteSign is a cipher suite for signing (no encryption). 12 | SuiteSign = SuiteSignV1 13 | // SuiteSignFile is a cipher suite for signing files (no encryption). 14 | SuiteSignFile = SuiteSignFileV1 15 | // SuiteComplete is a cipher suite for both encrypting for someone and signing. 16 | SuiteComplete = SuiteCompleteV1 17 | // SuiteWire is a cipher suite for network communication, including authentication of the server, but not the client. 18 | SuiteWire = SuiteWireV1 19 | ) 20 | 21 | // Suite Lists. 22 | var ( 23 | suitesMap = make(map[string]*Suite) 24 | suitesList []*Suite 25 | ) 26 | 27 | func registerSuite(suite *Suite) (suiteID string) { 28 | // add if not exists 29 | _, ok := suitesMap[suite.ID] 30 | if !ok { 31 | suitesMap[suite.ID] = suite 32 | suitesList = append(suitesList, suite) 33 | } 34 | 35 | return suite.ID 36 | } 37 | 38 | // GetSuite returns the suite with the given ID. 39 | func GetSuite(suiteID string) (suite *Suite, ok bool) { 40 | suite, ok = suitesMap[suiteID] 41 | return 42 | } 43 | 44 | // Suites returns all registered suites as a slice. 45 | func Suites() []*Suite { 46 | return suitesList 47 | } 48 | 49 | // SuitesMap returns all registered suites as a map. 50 | func SuitesMap() map[string]*Suite { 51 | return suitesMap 52 | } 53 | -------------------------------------------------------------------------------- /suites_v1.go: -------------------------------------------------------------------------------- 1 | package jess //nolint:dupl 2 | 3 | var ( 4 | // SuiteKeyV1 is a cipher suite for encryption with a key. 5 | SuiteKeyV1 = registerSuite(&Suite{ 6 | ID: "key_v1", 7 | Tools: []string{"HKDF(BLAKE2b-256)", "CHACHA20-POLY1305"}, 8 | Provides: NewRequirements(), 9 | SecurityLevel: 128, 10 | Status: SuiteStatusRecommended, 11 | }) 12 | // SuitePasswordV1 is a cipher suite for encryption with a password. 13 | SuitePasswordV1 = registerSuite(&Suite{ 14 | ID: "pw_v1", 15 | Tools: []string{"SCRYPT-20", "HKDF(BLAKE2b-256)", "CHACHA20-POLY1305"}, 16 | Provides: NewRequirements(), 17 | SecurityLevel: 128, 18 | Status: SuiteStatusRecommended, 19 | }) 20 | // SuiteRcptOnlyV1 is a cipher suite for encrypting for someone, but without verifying the sender/source. 21 | SuiteRcptOnlyV1 = registerSuite(&Suite{ 22 | ID: "rcpt_v1", 23 | Tools: []string{"ECDH-X25519", "HKDF(BLAKE2b-256)", "CHACHA20-POLY1305"}, 24 | Provides: NewRequirements().Remove(SenderAuthentication), 25 | SecurityLevel: 128, 26 | Status: SuiteStatusRecommended, 27 | }) 28 | // SuiteSignV1 is a cipher suite for signing (no encryption). 29 | SuiteSignV1 = registerSuite(&Suite{ 30 | ID: "sign_v1", 31 | Tools: []string{"Ed25519(BLAKE2b-256)"}, 32 | Provides: newEmptyRequirements().Add(Integrity).Add(SenderAuthentication), 33 | SecurityLevel: 128, 34 | Status: SuiteStatusRecommended, 35 | }) 36 | // SuiteSignFileV1 is a cipher suite for signing files (no encryption). 37 | // SHA2_256 is chosen for better compatibility with other tool sets and workflows. 38 | SuiteSignFileV1 = registerSuite(&Suite{ 39 | ID: "signfile_v1", 40 | Tools: []string{"Ed25519(SHA2-256)"}, 41 | Provides: newEmptyRequirements().Add(Integrity).Add(SenderAuthentication), 42 | SecurityLevel: 128, 43 | Status: SuiteStatusRecommended, 44 | }) 45 | // SuiteCompleteV1 is a cipher suite for both encrypting for someone and signing. 46 | SuiteCompleteV1 = registerSuite(&Suite{ 47 | ID: "v1", 48 | Tools: []string{"ECDH-X25519", "Ed25519(BLAKE2b-256)", "HKDF(BLAKE2b-256)", "CHACHA20-POLY1305"}, 49 | Provides: NewRequirements(), 50 | SecurityLevel: 128, 51 | Status: SuiteStatusRecommended, 52 | }) 53 | // SuiteWireV1 is a cipher suite for network communication, including authentication of the server, but not the client. 54 | SuiteWireV1 = registerSuite(&Suite{ 55 | ID: "w1", 56 | Tools: []string{"ECDH-X25519", "HKDF(BLAKE2b-256)", "CHACHA20-POLY1305"}, 57 | Provides: NewRequirements().Remove(SenderAuthentication), 58 | SecurityLevel: 128, 59 | Status: SuiteStatusRecommended, 60 | }) 61 | ) 62 | -------------------------------------------------------------------------------- /suites_v2.go: -------------------------------------------------------------------------------- 1 | package jess //nolint:dupl 2 | 3 | var ( 4 | // SuiteKeyV2 is a cipher suite for encryption with a key. 5 | SuiteKeyV2 = registerSuite(&Suite{ 6 | ID: "key_v2", 7 | Tools: []string{"BLAKE3-KDF", "CHACHA20-POLY1305"}, 8 | Provides: NewRequirements(), 9 | SecurityLevel: 128, 10 | Status: SuiteStatusPermitted, 11 | }) 12 | // SuitePasswordV2 is a cipher suite for encryption with a password. 13 | SuitePasswordV2 = registerSuite(&Suite{ 14 | ID: "pw_v2", 15 | Tools: []string{"SCRYPT-20", "BLAKE3-KDF", "CHACHA20-POLY1305"}, 16 | Provides: NewRequirements(), 17 | SecurityLevel: 128, 18 | Status: SuiteStatusPermitted, 19 | }) 20 | // SuiteRcptOnlyV2 is a cipher suite for encrypting for someone, but without verifying the sender/source. 21 | SuiteRcptOnlyV2 = registerSuite(&Suite{ 22 | ID: "rcpt_v2", 23 | Tools: []string{"ECDH-X25519", "BLAKE3-KDF", "CHACHA20-POLY1305"}, 24 | Provides: NewRequirements().Remove(SenderAuthentication), 25 | SecurityLevel: 128, 26 | Status: SuiteStatusPermitted, 27 | }) 28 | // SuiteSignV2 is a cipher suite for signing (no encryption). 29 | SuiteSignV2 = registerSuite(&Suite{ 30 | ID: "sign_v2", 31 | Tools: []string{"Ed25519(BLAKE3)"}, 32 | Provides: newEmptyRequirements().Add(Integrity).Add(SenderAuthentication), 33 | SecurityLevel: 128, 34 | Status: SuiteStatusPermitted, 35 | }) 36 | // SuiteSignFileV2 is a cipher suite for signing files (no encryption). 37 | // SHA2_256 is chosen for better compatibility with other tool sets and workflows. 38 | SuiteSignFileV2 = registerSuite(&Suite{ 39 | ID: "signfile_v2", 40 | Tools: []string{"Ed25519(BLAKE3)"}, 41 | Provides: newEmptyRequirements().Add(Integrity).Add(SenderAuthentication), 42 | SecurityLevel: 128, 43 | Status: SuiteStatusPermitted, 44 | }) 45 | // SuiteCompleteV2 is a cipher suite for both encrypting for someone and signing. 46 | SuiteCompleteV2 = registerSuite(&Suite{ 47 | ID: "v2", 48 | Tools: []string{"ECDH-X25519", "Ed25519(BLAKE3)", "BLAKE3-KDF", "CHACHA20-POLY1305"}, 49 | Provides: NewRequirements(), 50 | SecurityLevel: 128, 51 | Status: SuiteStatusPermitted, 52 | }) 53 | // SuiteWireV2 is a cipher suite for network communication, including authentication of the server, but not the client. 54 | SuiteWireV2 = registerSuite(&Suite{ 55 | ID: "w2", 56 | Tools: []string{"ECDH-X25519", "BLAKE3-KDF", "CHACHA20-POLY1305"}, 57 | Provides: NewRequirements().Remove(SenderAuthentication), 58 | SecurityLevel: 128, 59 | Status: SuiteStatusPermitted, 60 | }) 61 | ) 62 | -------------------------------------------------------------------------------- /supply/supply_test.go: -------------------------------------------------------------------------------- 1 | package supply 2 | 3 | import ( 4 | "testing" 5 | 6 | _ "github.com/safing/jess/tools/all" 7 | ) 8 | 9 | func TestSupply(t *testing.T) { 10 | t.Parallel() 11 | 12 | total := 10 13 | supply := NewSignetSupply(total) 14 | scheme := "ECDH-X25519" 15 | 16 | // get signet to initialize space 17 | _, err := supply.GetSignet(scheme) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | 22 | // fill one 23 | full, err := supply.Fill(1) 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | if full { 28 | t.Fatal("not expected to be full") 29 | } 30 | 31 | // take two 32 | for i := 0; i < 2; i++ { 33 | _, err := supply.GetSignet(scheme) 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | } 38 | 39 | // fill up 40 | full, err = supply.Fill(total + 1) 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | if !full { 45 | t.Fatal("expected to be full") 46 | } 47 | 48 | // empty all 49 | for i := 0; i < total; i++ { 50 | _, err := supply.GetSignet(scheme) 51 | if err != nil { 52 | t.Fatal(err) 53 | } 54 | } 55 | 56 | // fill and empty with different sizes 57 | for i := 0; i < total+3; i++ { 58 | // fill i 59 | _, err := supply.Fill(i) 60 | if err != nil { 61 | t.Fatal(err) 62 | } 63 | 64 | // empty total-i 65 | for j := 0; j < total+3-i; j++ { 66 | _, err := supply.GetSignet(scheme) 67 | if err != nil { 68 | t.Fatal(err) 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | warnings=0 4 | errors=0 5 | scripted=0 6 | goUp="\\e[1A" 7 | fullTestFlags="-short" 8 | install=0 9 | testonly=0 10 | 11 | function help { 12 | echo "usage: $0 [command] [options]" 13 | echo "" 14 | echo "commands:" 15 | echo " run baseline tests" 16 | echo " full run full tests (ie. not short)" 17 | echo " install install deps for running tests" 18 | echo "" 19 | echo "options:" 20 | echo " --scripted don't jump console lines (still use colors)" 21 | echo " --test-only run tests only, no linters" 22 | echo " [package] run only on this package" 23 | } 24 | 25 | function run { 26 | if [[ $scripted -eq 0 ]]; then 27 | echo "[......] $*" 28 | fi 29 | 30 | # create tmpfile 31 | tmpfile=$(mktemp) 32 | # execute 33 | $* >$tmpfile 2>&1 34 | rc=$? 35 | output=$(cat $tmpfile) 36 | 37 | # check return code 38 | if [[ $rc -eq 0 ]]; then 39 | if [[ $output == *"[no test files]"* ]]; then 40 | echo -e "${goUp}[\e[01;33mNOTEST\e[00m] $*" 41 | warnings=$((warnings+1)) 42 | else 43 | echo -ne "${goUp}[\e[01;32m OK \e[00m] " 44 | if [[ $2 == "test" ]]; then 45 | echo -n $* 46 | echo -n ": " 47 | echo $output | cut -f "3-" -d " " 48 | else 49 | echo $* 50 | fi 51 | fi 52 | else 53 | if [[ $output == *"build constraints exclude all Go files"* ]]; then 54 | echo -e "${goUp}[ !=OS ] $*" 55 | else 56 | echo -e "${goUp}[\e[01;31m FAIL \e[00m] $*" 57 | cat $tmpfile 58 | errors=$((errors+1)) 59 | fi 60 | fi 61 | 62 | rm -f $tmpfile 63 | } 64 | 65 | # get and switch to script dir 66 | baseDir="$( cd "$(dirname "$0")" && pwd )" 67 | cd "$baseDir" 68 | 69 | # args 70 | while true; do 71 | case "$1" in 72 | "-h"|"help"|"--help") 73 | help 74 | exit 0 75 | ;; 76 | "--scripted") 77 | scripted=1 78 | goUp="" 79 | shift 1 80 | ;; 81 | "--test-only") 82 | testonly=1 83 | shift 1 84 | ;; 85 | "install") 86 | install=1 87 | shift 1 88 | ;; 89 | "full") 90 | fullTestFlags="" 91 | shift 1 92 | ;; 93 | *) 94 | break 95 | ;; 96 | esac 97 | done 98 | 99 | # check if $GOPATH/bin is in $PATH 100 | if [[ $PATH != *"$GOPATH/bin"* ]]; then 101 | export PATH=$GOPATH/bin:$PATH 102 | fi 103 | 104 | # install 105 | if [[ $install -eq 1 ]]; then 106 | echo "installing dependencies..." 107 | # TODO: update golangci-lint version regularly 108 | echo "$ curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.44.0" 109 | curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.44.0 110 | exit 0 111 | fi 112 | 113 | # check dependencies 114 | if [[ $(which go) == "" ]]; then 115 | echo "go command not found" 116 | exit 1 117 | fi 118 | if [[ $testonly -eq 0 ]]; then 119 | if [[ $(which gofmt) == "" ]]; then 120 | echo "gofmt command not found" 121 | exit 1 122 | fi 123 | if [[ $(which golangci-lint) == "" ]]; then 124 | echo "golangci-lint command not found" 125 | echo "install with: curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin vX.Y.Z" 126 | echo "don't forget to specify the version you want" 127 | echo "or run: ./test install" 128 | echo "" 129 | echo "alternatively, install the current dev version with: go get -u github.com/golangci/golangci-lint/cmd/golangci-lint" 130 | exit 1 131 | fi 132 | fi 133 | 134 | # target selection 135 | if [[ "$1" == "" ]]; then 136 | # get all packages 137 | packages=$(go list -e ./...) 138 | else 139 | # single package testing 140 | packages=$(go list -e)/$1 141 | echo "note: only running tests for package $packages" 142 | fi 143 | 144 | # platform info 145 | platformInfo=$(go env GOOS GOARCH) 146 | echo "running tests for ${platformInfo//$'\n'/ }:" 147 | 148 | # run vet/test on packages 149 | for package in $packages; do 150 | packagename=${package#github.com/safing/jess} #TODO: could be queried with `go list .` 151 | packagename=${packagename#/} 152 | echo "" 153 | echo $package 154 | if [[ $testonly -eq 0 ]]; then 155 | run go vet $package 156 | run golangci-lint run $packagename 157 | fi 158 | run go test -cover $fullTestFlags $package 159 | done 160 | 161 | echo "" 162 | if [[ $errors -gt 0 ]]; then 163 | echo "failed with $errors errors and $warnings warnings" 164 | exit 1 165 | else 166 | echo "succeeded with $warnings warnings" 167 | exit 0 168 | fi 169 | -------------------------------------------------------------------------------- /tools.go: -------------------------------------------------------------------------------- 1 | package jess 2 | 3 | import ( 4 | "github.com/safing/jess/tools" 5 | // Import all tools. 6 | _ "github.com/safing/jess/tools/all" 7 | ) 8 | 9 | func init() { 10 | // init static logic 11 | for _, tool := range tools.AsList() { 12 | tool.StaticLogic = tool.Factory() 13 | tool.StaticLogic.Init( 14 | tool, 15 | &Helper{ 16 | session: nil, 17 | info: tool.Info, 18 | }, 19 | nil, 20 | nil, 21 | ) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tools/all/all.go: -------------------------------------------------------------------------------- 1 | // Package all imports all tool subpackages 2 | package all 3 | 4 | import ( 5 | // Import all tool subpackages. 6 | _ "github.com/safing/jess/tools/blake3" 7 | _ "github.com/safing/jess/tools/ecdh" 8 | _ "github.com/safing/jess/tools/gostdlib" 9 | ) 10 | -------------------------------------------------------------------------------- /tools/blake3/kdf.go: -------------------------------------------------------------------------------- 1 | package blake3 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | 8 | "github.com/zeebo/blake3" 9 | 10 | "github.com/safing/jess/tools" 11 | ) 12 | 13 | func init() { 14 | tools.Register(&tools.Tool{ 15 | Info: &tools.ToolInfo{ 16 | Name: "BLAKE3-KDF", 17 | Purpose: tools.PurposeKeyDerivation, 18 | SecurityLevel: 128, 19 | Comment: "cryptographic hash function based on Bao and BLAKE2", 20 | Author: "Jean-Philippe Aumasson et al., 2020", 21 | }, 22 | Factory: func() tools.ToolLogic { return &KDF{} }, 23 | }) 24 | } 25 | 26 | // KDF implements the cryptographic interface for BLAKE3 key derivation. 27 | type KDF struct { 28 | tools.ToolLogicBase 29 | reader io.Reader 30 | } 31 | 32 | // InitKeyDerivation implements the ToolLogic interface. 33 | func (keyder *KDF) InitKeyDerivation(nonce []byte, material ...[]byte) error { 34 | // Check params. 35 | if len(material) < 1 || len(material[0]) == 0 || len(nonce) == 0 { 36 | return errors.New("must supply at least one key and a nonce as key material") 37 | } 38 | 39 | // Setup KDF. 40 | // Use nonce as kdf context. 41 | h := blake3.NewDeriveKey(string(nonce)) 42 | // Then add all the key material. 43 | for _, m := range material { 44 | _, _ = h.Write(m) 45 | } 46 | // Get key reader. 47 | keyder.reader = h.Digest() 48 | 49 | return nil 50 | } 51 | 52 | // DeriveKey implements the ToolLogic interface. 53 | func (keyder *KDF) DeriveKey(size int) ([]byte, error) { 54 | key := make([]byte, size) 55 | return key, keyder.DeriveKeyWriteTo(key) 56 | } 57 | 58 | // DeriveKeyWriteTo implements the ToolLogic interface. 59 | func (keyder *KDF) DeriveKeyWriteTo(newKey []byte) error { 60 | n, err := io.ReadFull(keyder.reader, newKey) 61 | if err != nil { 62 | return fmt.Errorf("failed to generate key: %w", err) 63 | } 64 | if n != len(newKey) { 65 | return errors.New("failed to generate key: EOF") 66 | } 67 | return nil 68 | } 69 | -------------------------------------------------------------------------------- /tools/ecdh/nist.go: -------------------------------------------------------------------------------- 1 | package ecdh 2 | 3 | import ( 4 | "crypto" 5 | "crypto/elliptic" 6 | "fmt" 7 | "math/big" 8 | 9 | "github.com/aead/ecdh" 10 | 11 | "github.com/safing/jess/tools" 12 | "github.com/safing/structures/container" 13 | ) 14 | 15 | var nistCurveInfo = &tools.ToolInfo{ 16 | Purpose: tools.PurposeKeyExchange, 17 | Comment: "FIPS 186", 18 | Author: "NIST, 2009", 19 | } 20 | 21 | func init() { 22 | tools.Register(&tools.Tool{ 23 | Info: nistCurveInfo.With(&tools.ToolInfo{ 24 | Name: "ECDH-P224", 25 | SecurityLevel: 112, 26 | }), 27 | Factory: func() tools.ToolLogic { return &NistCurve{curve: ecdh.Generic(elliptic.P224())} }, 28 | }) 29 | tools.Register(&tools.Tool{ 30 | Info: nistCurveInfo.With(&tools.ToolInfo{ 31 | Name: "ECDH-P256", 32 | SecurityLevel: 128, 33 | }), 34 | Factory: func() tools.ToolLogic { return &NistCurve{curve: ecdh.Generic(elliptic.P256())} }, 35 | }) 36 | tools.Register(&tools.Tool{ 37 | Info: nistCurveInfo.With(&tools.ToolInfo{ 38 | Name: "ECDH-P384", 39 | SecurityLevel: 192, 40 | }), 41 | Factory: func() tools.ToolLogic { return &NistCurve{curve: ecdh.Generic(elliptic.P384())} }, 42 | }) 43 | tools.Register(&tools.Tool{ 44 | Info: nistCurveInfo.With(&tools.ToolInfo{ 45 | Name: "ECDH-P521", 46 | SecurityLevel: 256, 47 | }), 48 | Factory: func() tools.ToolLogic { return &NistCurve{curve: ecdh.Generic(elliptic.P521())} }, 49 | }) 50 | } 51 | 52 | // NistCurve implements the cryptographic interface for ECDH key exchange with NIST curves. 53 | type NistCurve struct { 54 | tools.ToolLogicBase 55 | curve ecdh.KeyExchange 56 | } 57 | 58 | // MakeSharedKey implements the ToolLogic interface. 59 | func (ec *NistCurve) MakeSharedKey(local tools.SignetInt, remote tools.SignetInt) ([]byte, error) { 60 | return ec.curve.ComputeSecret(local.PrivateKey(), remote.PublicKey()), nil 61 | } 62 | 63 | // LoadKey implements the ToolLogic interface. 64 | func (ec *NistCurve) LoadKey(signet tools.SignetInt) error { 65 | var pubKey crypto.PublicKey 66 | var privKey crypto.PrivateKey 67 | 68 | key, public := signet.GetStoredKey() 69 | c := container.New(key) 70 | 71 | // check serialization version 72 | version, err := c.GetNextN8() 73 | if err != nil || version != 1 { 74 | return tools.ErrInvalidKey 75 | } 76 | 77 | // load public key 78 | // extract public key data 79 | pointXData, err := c.GetNextBlock() 80 | if err != nil { 81 | return err 82 | } 83 | pointYData, err := c.GetNextBlock() 84 | if err != nil { 85 | return err 86 | } 87 | // transform public key data 88 | point := ecdh.Point{} 89 | point.X = new(big.Int).SetBytes(pointXData) 90 | point.Y = new(big.Int).SetBytes(pointYData) 91 | pubKey = point 92 | 93 | // check public key 94 | err = ec.curve.Check(pubKey) 95 | if err != nil { 96 | return tools.ErrInvalidKey 97 | } 98 | 99 | // load private key 100 | if !public { 101 | privKey = c.CompileData() 102 | } 103 | 104 | signet.SetLoadedKeys(pubKey, privKey) 105 | return nil 106 | } 107 | 108 | // StoreKey implements the ToolLogic interface. 109 | func (ec *NistCurve) StoreKey(signet tools.SignetInt) error { 110 | pubKey := signet.PublicKey() 111 | privKey := signet.PrivateKey() 112 | public := privKey == nil 113 | 114 | // create storage with serialization version 115 | c := container.New() 116 | c.AppendNumber(1) 117 | 118 | // store public key 119 | curvePoint, ok := pubKey.(ecdh.Point) 120 | if !ok { 121 | return fmt.Errorf("public key of invalid type %T", pubKey) 122 | } 123 | c.AppendAsBlock(curvePoint.X.Bytes()) 124 | c.AppendAsBlock(curvePoint.Y.Bytes()) 125 | 126 | // store private key 127 | if !public { 128 | privKeyData, ok := privKey.([]byte) 129 | if !ok { 130 | return fmt.Errorf("private key of invalid type %T", privKey) 131 | } 132 | c.Append(privKeyData) 133 | } 134 | 135 | signet.SetStoredKey(c.CompileData(), public) 136 | return nil 137 | } 138 | 139 | // GenerateKey implements the ToolLogic interface. 140 | func (ec *NistCurve) GenerateKey(signet tools.SignetInt) error { 141 | // define variable types for API security 142 | var pubKey crypto.PublicKey 143 | var privKey crypto.PrivateKey 144 | var err error 145 | 146 | // generate keys 147 | privKey, pubKey, err = ec.curve.GenerateKey(ec.Helper().Random()) 148 | if err != nil { 149 | return err 150 | } 151 | 152 | signet.SetLoadedKeys(pubKey, privKey) 153 | return nil 154 | } 155 | 156 | // BurnKey implements the ToolLogic interface. This is currently ineffective, see known issues in the project's README. 157 | func (ec *NistCurve) BurnKey(signet tools.SignetInt) error { 158 | pubKey := signet.PublicKey() 159 | privKey := signet.PrivateKey() 160 | 161 | // burn public key 162 | if pubKey != nil { 163 | point, ok := pubKey.(*ecdh.Point) 164 | if ok { 165 | point.X.Set(big.NewInt(0)) 166 | point.Y.Set(big.NewInt(0)) 167 | } 168 | } 169 | 170 | // burn private key 171 | if privKey != nil { 172 | data, ok := privKey.([]byte) 173 | if ok { 174 | ec.Helper().Burn(data) 175 | } 176 | } 177 | 178 | return nil 179 | } 180 | -------------------------------------------------------------------------------- /tools/ecdh/x25519.go: -------------------------------------------------------------------------------- 1 | package ecdh 2 | 3 | import ( 4 | "crypto" 5 | "fmt" 6 | 7 | "github.com/aead/ecdh" 8 | 9 | "github.com/safing/jess/tools" 10 | "github.com/safing/structures/container" 11 | ) 12 | 13 | func init() { 14 | tools.Register(&tools.Tool{ 15 | Info: &tools.ToolInfo{ 16 | Name: "ECDH-X25519", 17 | Purpose: tools.PurposeKeyExchange, 18 | SecurityLevel: 128, 19 | Comment: "", 20 | Author: "Daniel J. Bernstein, 2005", 21 | }, 22 | Factory: func() tools.ToolLogic { return &X25519Curve{} }, 23 | }) 24 | } 25 | 26 | // X25519Curve implements the cryptographic interface for the ECDH X25519 key exchange. 27 | type X25519Curve struct { 28 | tools.ToolLogicBase 29 | } 30 | 31 | // MakeSharedKey implements the ToolLogic interface. 32 | func (ec *X25519Curve) MakeSharedKey(local tools.SignetInt, remote tools.SignetInt) ([]byte, error) { 33 | return ecdh.X25519().ComputeSecret(local.PrivateKey(), remote.PublicKey()), nil 34 | } 35 | 36 | // LoadKey implements the ToolLogic interface. 37 | func (ec *X25519Curve) LoadKey(signet tools.SignetInt) error { 38 | var pubKey crypto.PublicKey 39 | var privKey crypto.PrivateKey 40 | 41 | key, public := signet.GetStoredKey() 42 | c := container.New(key) 43 | 44 | // check serialization version 45 | version, err := c.GetNextN8() 46 | if err != nil || version != 1 { 47 | return tools.ErrInvalidKey 48 | } 49 | 50 | // load public key 51 | data, err := c.Get(32) 52 | if err != nil { 53 | return tools.ErrInvalidKey 54 | } 55 | var pubKeyData [32]byte 56 | copy(pubKeyData[:], data) 57 | pubKey = pubKeyData 58 | 59 | // check public key 60 | err = ecdh.X25519().Check(pubKey) 61 | if err != nil { 62 | return tools.ErrInvalidKey 63 | } 64 | 65 | // load private key 66 | if !public { 67 | data, err = c.Get(32) 68 | if err != nil { 69 | return tools.ErrInvalidKey 70 | } 71 | var privKeyData [32]byte 72 | copy(privKeyData[:], data) 73 | privKey = privKeyData 74 | } 75 | 76 | signet.SetLoadedKeys(pubKey, privKey) 77 | return nil 78 | } 79 | 80 | // StoreKey implements the ToolLogic interface. 81 | func (ec *X25519Curve) StoreKey(signet tools.SignetInt) error { 82 | pubKey := signet.PublicKey() 83 | privKey := signet.PrivateKey() 84 | public := privKey == nil 85 | 86 | // create storage with serialization version 87 | c := container.New() 88 | c.AppendNumber(1) 89 | 90 | // store keys 91 | pubKeyData, ok := pubKey.([32]byte) 92 | if !ok { 93 | return fmt.Errorf("public key of invalid type %T", pubKey) 94 | } 95 | c.Append(pubKeyData[:]) 96 | if !public { 97 | privKeyData, ok := privKey.([32]byte) 98 | if !ok { 99 | return fmt.Errorf("private key of invalid type %T", privKey) 100 | } 101 | c.Append(privKeyData[:]) 102 | } 103 | 104 | signet.SetStoredKey(c.CompileData(), public) 105 | return nil 106 | } 107 | 108 | // GenerateKey implements the ToolLogic interface. 109 | func (ec *X25519Curve) GenerateKey(signet tools.SignetInt) error { 110 | // define variable types for API security 111 | var pubKey crypto.PublicKey 112 | var privKey crypto.PrivateKey 113 | var err error 114 | 115 | // generate keys 116 | privKey, pubKey, err = ecdh.X25519().GenerateKey(ec.Helper().Random()) 117 | if err != nil { 118 | return err 119 | } 120 | 121 | signet.SetLoadedKeys(pubKey, privKey) 122 | return nil 123 | } 124 | 125 | // BurnKey implements the ToolLogic interface. This is currently ineffective, see known issues in the project's README. 126 | func (ec *X25519Curve) BurnKey(signet tools.SignetInt) error { 127 | pubKey := signet.PublicKey() 128 | privKey := signet.PrivateKey() 129 | 130 | // burn public key 131 | if pubKey != nil { 132 | data, ok := pubKey.([32]byte) 133 | if ok { 134 | ec.Helper().Burn(data[:]) 135 | } 136 | } 137 | 138 | // burn private key 139 | if privKey != nil { 140 | data, ok := privKey.([32]byte) 141 | if ok { 142 | ec.Helper().Burn(data[:]) 143 | } 144 | } 145 | 146 | return nil 147 | } 148 | -------------------------------------------------------------------------------- /tools/errors.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import "errors" 4 | 5 | var ( 6 | // ErrNotFound is returned when a tool cannot be found. 7 | ErrNotFound = errors.New("does not exist") 8 | 9 | // ErrInvalidKey is returned when a invalid public or private key was supplied. 10 | ErrInvalidKey = errors.New("invalid key") 11 | 12 | // ErrNotImplemented is returned by the dummy functions if they are not overridden correctly. 13 | ErrNotImplemented = errors.New("not implemented") 14 | 15 | // ErrProtected is returned if an operation is executed, but the key is still protected. 16 | ErrProtected = errors.New("key is protected") 17 | ) 18 | -------------------------------------------------------------------------------- /tools/gostdlib/aes-ctr.go: -------------------------------------------------------------------------------- 1 | package gostdlib 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/cipher" 6 | 7 | "github.com/safing/jess/tools" 8 | ) 9 | 10 | func init() { 11 | aesCtrInfo := &tools.ToolInfo{ 12 | Purpose: tools.PurposeCipher, 13 | Options: []uint8{tools.OptionHasState}, 14 | NonceSize: aes.BlockSize, 15 | Comment: "aka Rijndael, FIPS 197", 16 | Author: "Vincent Rijmen and Joan Daemen, 1998", 17 | } 18 | aesCtrFactory := func() tools.ToolLogic { return &AesCTR{} } 19 | 20 | tools.Register(&tools.Tool{ 21 | Info: aesCtrInfo.With(&tools.ToolInfo{ 22 | Name: "AES128-CTR", 23 | KeySize: 16, // 128 bits 24 | SecurityLevel: 128, 25 | }), 26 | Factory: aesCtrFactory, 27 | }) 28 | tools.Register(&tools.Tool{ 29 | Info: aesCtrInfo.With(&tools.ToolInfo{ 30 | Name: "AES192-CTR", 31 | KeySize: 24, // 192 bits 32 | SecurityLevel: 192, 33 | }), 34 | Factory: aesCtrFactory, 35 | }) 36 | tools.Register(&tools.Tool{ 37 | Info: aesCtrInfo.With(&tools.ToolInfo{ 38 | Name: "AES256-CTR", 39 | KeySize: 32, // 256 bits 40 | SecurityLevel: 256, 41 | }), 42 | Factory: aesCtrFactory, 43 | }) 44 | } 45 | 46 | // AesCTR implements the cryptographic interface for AES-CTR encryption. 47 | type AesCTR struct { 48 | tools.ToolLogicBase 49 | stream cipher.Stream 50 | key, iv []byte 51 | } 52 | 53 | // Setup implements the ToolLogic interface. 54 | func (aesctr *AesCTR) Setup() (err error) { 55 | // get key 56 | aesctr.key, err = aesctr.Helper().NewSessionKey() 57 | if err != nil { 58 | return err 59 | } 60 | 61 | // get IV 62 | aesctr.iv, err = aesctr.Helper().NewSessionNonce() 63 | if err != nil { 64 | return err 65 | } 66 | 67 | // get cipher.Block 68 | block, err := aes.NewCipher(aesctr.key) 69 | if err != nil { 70 | return err 71 | } 72 | 73 | // get cipher.Stream 74 | aesctr.stream = cipher.NewCTR(block, aesctr.iv) 75 | 76 | return nil 77 | } 78 | 79 | // Reset implements the ToolLogic interface. 80 | func (aesctr *AesCTR) Reset() error { 81 | // clean up keys 82 | aesctr.Helper().Burn(aesctr.key) 83 | aesctr.Helper().Burn(aesctr.iv) 84 | 85 | return nil 86 | } 87 | 88 | // Encrypt implements the ToolLogic interface. 89 | func (aesctr *AesCTR) Encrypt(data []byte) ([]byte, error) { 90 | // encrypt 91 | aesctr.stream.XORKeyStream(data, data) 92 | 93 | return data, nil 94 | } 95 | 96 | // Decrypt implements the ToolLogic interface. 97 | func (aesctr *AesCTR) Decrypt(data []byte) ([]byte, error) { 98 | // decrypt 99 | aesctr.stream.XORKeyStream(data, data) 100 | 101 | return data, nil 102 | } 103 | -------------------------------------------------------------------------------- /tools/gostdlib/aes-gcm.go: -------------------------------------------------------------------------------- 1 | package gostdlib 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/cipher" 6 | 7 | "github.com/safing/jess/tools" 8 | ) 9 | 10 | func init() { 11 | aesGcmInfo := &tools.ToolInfo{ 12 | Purpose: tools.PurposeIntegratedCipher, 13 | Options: []uint8{tools.OptionHasState}, 14 | NonceSize: 12, // standard nonce size for GCM in Golang stdlib 15 | Comment: "aka Rijndael, FIPS 197", 16 | Author: "Vincent Rijmen and Joan Daemen, 1998", 17 | } 18 | aesGcmFactory := func() tools.ToolLogic { return &AesGCM{} } 19 | 20 | tools.Register(&tools.Tool{ 21 | Info: aesGcmInfo.With(&tools.ToolInfo{ 22 | Name: "AES128-GCM", 23 | KeySize: 16, // 128 bits 24 | SecurityLevel: 128, 25 | }), 26 | Factory: aesGcmFactory, 27 | }) 28 | tools.Register(&tools.Tool{ 29 | Info: aesGcmInfo.With(&tools.ToolInfo{ 30 | Name: "AES192-GCM", 31 | KeySize: 24, // 192 bits 32 | SecurityLevel: 192, 33 | }), 34 | Factory: aesGcmFactory, 35 | }) 36 | tools.Register(&tools.Tool{ 37 | Info: aesGcmInfo.With(&tools.ToolInfo{ 38 | Name: "AES256-GCM", 39 | KeySize: 32, // 256 bits 40 | SecurityLevel: 256, 41 | }), 42 | Factory: aesGcmFactory, 43 | }) 44 | } 45 | 46 | // AesGCM implements the cryptographic interface for AES-GCM encryption. 47 | type AesGCM struct { 48 | tools.ToolLogicBase 49 | aead cipher.AEAD 50 | key, nonce []byte 51 | } 52 | 53 | // Setup implements the ToolLogic interface. 54 | func (aesgcm *AesGCM) Setup() (err error) { 55 | // get key 56 | aesgcm.key, err = aesgcm.Helper().NewSessionKey() 57 | if err != nil { 58 | return err 59 | } 60 | 61 | // get nonce 62 | aesgcm.nonce, err = aesgcm.Helper().NewSessionNonce() 63 | if err != nil { 64 | return err 65 | } 66 | 67 | // get cipher.Block 68 | block, err := aes.NewCipher(aesgcm.key) 69 | if err != nil { 70 | return err 71 | } 72 | 73 | // get aead interface 74 | aesgcm.aead, err = cipher.NewGCM(block) 75 | if err != nil { 76 | return err 77 | } 78 | 79 | aesgcm.aead.NonceSize() 80 | 81 | return nil 82 | } 83 | 84 | // Reset implements the ToolLogic interface. 85 | func (aesgcm *AesGCM) Reset() error { 86 | // clean up keys 87 | aesgcm.Helper().Burn(aesgcm.key) 88 | aesgcm.Helper().Burn(aesgcm.nonce) 89 | 90 | return nil 91 | } 92 | 93 | // AuthenticatedEncrypt implements the ToolLogic interface. 94 | func (aesgcm *AesGCM) AuthenticatedEncrypt(data, associatedData []byte) ([]byte, error) { 95 | // encrypt and authenticate 96 | data = aesgcm.aead.Seal(data[:0], aesgcm.nonce, data, associatedData) 97 | 98 | return data, nil 99 | } 100 | 101 | // AuthenticatedDecrypt implements the ToolLogic interface. 102 | func (aesgcm *AesGCM) AuthenticatedDecrypt(data, associatedData []byte) ([]byte, error) { 103 | // decrypt and authenticate 104 | var err error 105 | data, err = aesgcm.aead.Open(data[:0], aesgcm.nonce, data, associatedData) 106 | 107 | return data, err 108 | } 109 | -------------------------------------------------------------------------------- /tools/gostdlib/chacha20-poly1305.go: -------------------------------------------------------------------------------- 1 | package gostdlib 2 | 3 | import ( 4 | "crypto/cipher" 5 | 6 | "golang.org/x/crypto/chacha20poly1305" 7 | 8 | "github.com/safing/jess/tools" 9 | ) 10 | 11 | func init() { 12 | tools.Register(&tools.Tool{ 13 | Info: &tools.ToolInfo{ 14 | Name: "CHACHA20-POLY1305", 15 | Purpose: tools.PurposeIntegratedCipher, 16 | Options: []uint8{tools.OptionHasState}, 17 | KeySize: chacha20poly1305.KeySize, // 256 bit 18 | NonceSize: chacha20poly1305.NonceSize, 19 | SecurityLevel: 128, // ChaCha20 is actually 256. Limiting to 128 for now because of Poly1305. TODO: do some more research on Poly1305 20 | Comment: "RFC 7539", 21 | Author: "Daniel J. Bernstein, 2008 and 2005", 22 | }, 23 | Factory: func() tools.ToolLogic { return &ChaCha20Poly1305{} }, 24 | }) 25 | } 26 | 27 | // ChaCha20Poly1305 implements the cryptographic interface for ChaCha20-Poly1305 encryption. 28 | type ChaCha20Poly1305 struct { 29 | tools.ToolLogicBase 30 | aead cipher.AEAD 31 | key, nonce []byte 32 | } 33 | 34 | // Setup implements the ToolLogic interface. 35 | func (chapo *ChaCha20Poly1305) Setup() (err error) { 36 | // get key 37 | chapo.key, err = chapo.Helper().NewSessionKey() 38 | if err != nil { 39 | return err 40 | } 41 | 42 | // get nonce 43 | chapo.nonce, err = chapo.Helper().NewSessionNonce() 44 | if err != nil { 45 | return err 46 | } 47 | 48 | // get aead interface 49 | chapo.aead, err = chacha20poly1305.New(chapo.key) 50 | if err != nil { 51 | return err 52 | } 53 | 54 | return nil 55 | } 56 | 57 | // Reset implements the ToolLogic interface. 58 | func (chapo *ChaCha20Poly1305) Reset() error { 59 | // clean up keys 60 | chapo.Helper().Burn(chapo.key) 61 | chapo.Helper().Burn(chapo.nonce) 62 | 63 | return nil 64 | } 65 | 66 | // AuthenticatedEncrypt implements the ToolLogic interface. 67 | func (chapo *ChaCha20Poly1305) AuthenticatedEncrypt(data, associatedData []byte) ([]byte, error) { 68 | // encrypt and authenticate 69 | data = chapo.aead.Seal(data[:0], chapo.nonce, data, associatedData) 70 | 71 | return data, nil 72 | } 73 | 74 | // AuthenticatedDecrypt implements the ToolLogic interface. 75 | func (chapo *ChaCha20Poly1305) AuthenticatedDecrypt(data, associatedData []byte) ([]byte, error) { 76 | // decrypt and authenticate 77 | var err error 78 | data, err = chapo.aead.Open(data[:0], chapo.nonce, data, associatedData) 79 | 80 | return data, err 81 | } 82 | -------------------------------------------------------------------------------- /tools/gostdlib/ed25519.go: -------------------------------------------------------------------------------- 1 | package gostdlib 2 | 3 | import ( 4 | "crypto" 5 | "crypto/ed25519" 6 | "errors" 7 | 8 | "github.com/safing/jess/tools" 9 | "github.com/safing/structures/container" 10 | ) 11 | 12 | func init() { 13 | tools.Register(&tools.Tool{ 14 | Info: &tools.ToolInfo{ 15 | Name: "Ed25519", 16 | Purpose: tools.PurposeSigning, 17 | Options: []uint8{tools.OptionNeedsManagedHasher}, 18 | SecurityLevel: 128, 19 | Comment: "", 20 | Author: "Daniel J. Bernstein, 2011", 21 | }, 22 | Factory: func() tools.ToolLogic { return &Ed25519{} }, 23 | }) 24 | } 25 | 26 | // Ed25519 implements the cryptographic interface for Ed25519 signatures. 27 | type Ed25519 struct { 28 | tools.ToolLogicBase 29 | } 30 | 31 | // Sign implements the ToolLogic interface. 32 | func (ed *Ed25519) Sign(data, associatedData []byte, signet tools.SignetInt) ([]byte, error) { 33 | edPrivKey, ok := signet.PrivateKey().(ed25519.PrivateKey) 34 | if !ok { 35 | return nil, tools.ErrInvalidKey 36 | } 37 | if len(edPrivKey) != ed25519.PrivateKeySize { 38 | return nil, tools.ErrInvalidKey 39 | } 40 | 41 | hashsum, err := ed.ManagedHashSum() 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | return ed25519.Sign(edPrivKey, hashsum), nil 47 | } 48 | 49 | // Verify implements the ToolLogic interface. 50 | func (ed *Ed25519) Verify(data, associatedData, signature []byte, signet tools.SignetInt) error { 51 | edPubKey, ok := signet.PublicKey().(ed25519.PublicKey) 52 | if !ok { 53 | return tools.ErrInvalidKey 54 | } 55 | if len(edPubKey) != ed25519.PublicKeySize { 56 | return tools.ErrInvalidKey 57 | } 58 | 59 | hashsum, err := ed.ManagedHashSum() 60 | if err != nil { 61 | return err 62 | } 63 | 64 | if !ed25519.Verify(edPubKey, hashsum, signature) { 65 | return errors.New("signature invalid") 66 | } 67 | return nil 68 | } 69 | 70 | // LoadKey implements the ToolLogic interface. 71 | func (ed *Ed25519) LoadKey(signet tools.SignetInt) error { 72 | var pubKey crypto.PublicKey 73 | var privKey ed25519.PrivateKey 74 | 75 | key, public := signet.GetStoredKey() 76 | c := container.New(key) 77 | 78 | // check serialization version 79 | version, err := c.GetNextN8() 80 | if err != nil || version != 1 { 81 | return tools.ErrInvalidKey 82 | } 83 | 84 | // load public key 85 | data := c.CompileData() 86 | 87 | // assign and check data 88 | if public { 89 | if len(data) != ed25519.PublicKeySize { 90 | return tools.ErrInvalidKey 91 | } 92 | pubKey = ed25519.PublicKey(data) 93 | } else { 94 | if len(data) != ed25519.PrivateKeySize { 95 | return tools.ErrInvalidKey 96 | } 97 | privKey = ed25519.PrivateKey(data) 98 | pubKey = privKey.Public() 99 | } 100 | 101 | signet.SetLoadedKeys(pubKey, privKey) 102 | return nil 103 | } 104 | 105 | // StoreKey implements the ToolLogic interface. 106 | func (ed *Ed25519) StoreKey(signet tools.SignetInt) error { 107 | pubKey := signet.PublicKey() 108 | privKey := signet.PrivateKey() 109 | public := privKey == nil 110 | 111 | // create storage with serialization version 112 | c := container.New() 113 | c.AppendNumber(1) 114 | 115 | // store keys 116 | if public { 117 | pubKeyData, ok := pubKey.(ed25519.PublicKey) 118 | if !ok { 119 | return tools.ErrInvalidKey 120 | } 121 | c.Append(pubKeyData) 122 | } else { 123 | privKeyData, ok := privKey.(ed25519.PrivateKey) 124 | if !ok { 125 | return tools.ErrInvalidKey 126 | } 127 | c.Append(privKeyData) 128 | } 129 | 130 | signet.SetStoredKey(c.CompileData(), public) 131 | return nil 132 | } 133 | 134 | // GenerateKey implements the ToolLogic interface. 135 | func (ed *Ed25519) GenerateKey(signet tools.SignetInt) error { 136 | // define variable types for API security 137 | var pubKey ed25519.PublicKey 138 | var privKey ed25519.PrivateKey 139 | var err error 140 | 141 | // generate keys 142 | pubKey, privKey, err = ed25519.GenerateKey(ed.Helper().Random()) 143 | if err != nil { 144 | return err 145 | } 146 | 147 | signet.SetLoadedKeys(pubKey, privKey) 148 | return nil 149 | } 150 | 151 | // BurnKey implements the ToolLogic interface. This is currently ineffective, see known issues in the project's README. 152 | func (ed *Ed25519) BurnKey(signet tools.SignetInt) error { 153 | pubKey := signet.PublicKey() 154 | privKey := signet.PrivateKey() 155 | 156 | // burn public key 157 | if pubKey != nil { 158 | data, ok := pubKey.([]byte) 159 | if ok { 160 | ed.Helper().Burn(data) 161 | } 162 | } 163 | 164 | // burn private key 165 | if privKey != nil { 166 | data, ok := privKey.([]byte) 167 | if ok { 168 | ed.Helper().Burn(data) 169 | } 170 | } 171 | 172 | return nil 173 | } 174 | -------------------------------------------------------------------------------- /tools/gostdlib/hkdf.go: -------------------------------------------------------------------------------- 1 | package gostdlib 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | 8 | "golang.org/x/crypto/hkdf" 9 | 10 | "github.com/safing/jess/tools" 11 | "github.com/safing/structures/container" 12 | ) 13 | 14 | func init() { 15 | tools.Register(&tools.Tool{ 16 | Info: &tools.ToolInfo{ 17 | Name: "HKDF", 18 | Purpose: tools.PurposeKeyDerivation, 19 | Options: []uint8{tools.OptionNeedsDedicatedHasher}, 20 | SecurityLevel: 0, // depends on used hash function 21 | Comment: "RFC 5869", 22 | Author: "Hugo Krawczyk, 2010", 23 | }, 24 | Factory: func() tools.ToolLogic { return &HKDF{} }, 25 | }) 26 | } 27 | 28 | // HKDF implements the cryptographic interface for HKDF key derivation. 29 | type HKDF struct { 30 | tools.ToolLogicBase 31 | reader io.Reader 32 | } 33 | 34 | // InitKeyDerivation implements the ToolLogic interface. 35 | func (keyder *HKDF) InitKeyDerivation(nonce []byte, material ...[]byte) error { 36 | // hkdf arguments: hash func() hash.Hash, secret, salt, info []byte 37 | // `secret` and `salt` are used for the initial `extract` operation 38 | // `info` is mixed into every `expand` operation 39 | if len(material) < 1 || len(material[0]) == 0 || len(nonce) == 0 { 40 | return errors.New("must supply at least one key and a nonce as key material") 41 | } 42 | 43 | keyder.reader = hkdf.New( 44 | keyder.HashTool().New, 45 | container.New(material...).CompileData(), // cryptographically secure master secret(s) 46 | nonce, // non-secret salt 47 | nil, // non-secret info 48 | ) 49 | return nil 50 | } 51 | 52 | // DeriveKey implements the ToolLogic interface. 53 | func (keyder *HKDF) DeriveKey(size int) ([]byte, error) { 54 | key := make([]byte, size) 55 | return key, keyder.DeriveKeyWriteTo(key) 56 | } 57 | 58 | // DeriveKeyWriteTo implements the ToolLogic interface. 59 | func (keyder *HKDF) DeriveKeyWriteTo(newKey []byte) error { 60 | n, err := io.ReadFull(keyder.reader, newKey) 61 | if err != nil { 62 | return fmt.Errorf("failed to generate key: %w", err) 63 | } 64 | if n != len(newKey) { 65 | return errors.New("failed to generate key: EOF") 66 | } 67 | return nil 68 | } 69 | -------------------------------------------------------------------------------- /tools/gostdlib/hmac.go: -------------------------------------------------------------------------------- 1 | package gostdlib 2 | 3 | import ( 4 | "crypto/hmac" 5 | "errors" 6 | 7 | "github.com/safing/jess/tools" 8 | ) 9 | 10 | func init() { 11 | tools.Register(&tools.Tool{ 12 | Info: &tools.ToolInfo{ 13 | Name: "HMAC", 14 | Purpose: tools.PurposeMAC, 15 | Options: []uint8{ 16 | tools.OptionNeedsDedicatedHasher, 17 | tools.OptionHasState, 18 | }, 19 | SecurityLevel: 0, // depends on used hash function 20 | Comment: "RFC 2104, FIPS 198", 21 | Author: "Mihir Bellare et al., 1996", 22 | }, 23 | Factory: func() tools.ToolLogic { return &HMAC{} }, 24 | }) 25 | } 26 | 27 | // HMAC implements the cryptographic interface for HMAC message authentication codes. 28 | type HMAC struct { 29 | tools.ToolLogicBase 30 | key []byte 31 | } 32 | 33 | // Setup implements the ToolLogic interface. 34 | func (hm *HMAC) Setup() (err error) { 35 | // get key 36 | hm.key, err = hm.Helper().NewSessionKey() 37 | if err != nil { 38 | return err 39 | } 40 | 41 | return nil 42 | } 43 | 44 | // Reset implements the ToolLogic interface. 45 | func (hm *HMAC) Reset() error { 46 | // clean up key 47 | hm.Helper().Burn(hm.key) 48 | 49 | return nil 50 | } 51 | 52 | // MAC implements the ToolLogic interface. 53 | func (hm *HMAC) MAC(data, associatedData []byte) ([]byte, error) { 54 | // create MAC 55 | mac := hmac.New(hm.HashTool().New, hm.key) 56 | // write data 57 | n, err := mac.Write(data) 58 | if err != nil { 59 | return nil, err 60 | } 61 | if n != len(data) { 62 | return nil, errors.New("failed to fully write data to HMAC hasher") 63 | } 64 | // write associated data 65 | if len(associatedData) > 0 { 66 | n, err := mac.Write(associatedData) 67 | if err != nil { 68 | return nil, err 69 | } 70 | if n != len(associatedData) { 71 | return nil, errors.New("failed to fully write associated data to HMAC hasher") 72 | } 73 | } 74 | 75 | // return sum 76 | return mac.Sum(nil), nil 77 | } 78 | -------------------------------------------------------------------------------- /tools/gostdlib/pbkdf2.go: -------------------------------------------------------------------------------- 1 | package gostdlib 2 | 3 | import ( 4 | "crypto/sha256" 5 | "hash" 6 | 7 | "golang.org/x/crypto/pbkdf2" 8 | 9 | "github.com/safing/jess/tools" 10 | ) 11 | 12 | func init() { 13 | tools.Register(&tools.Tool{ 14 | Info: &tools.ToolInfo{ 15 | Name: "PBKDF2-SHA2-256", 16 | Purpose: tools.PurposePassDerivation, 17 | Options: []uint8{tools.OptionNeedsDefaultKeySize}, 18 | SecurityLevel: 0, // Security Level of SHA2-256 19 | Comment: "PKCS #5 v2.1, RFC 8018", 20 | Author: "Burt Kaliski, RSA Laboratories, 2000/2017", 21 | }, 22 | Factory: func() tools.ToolLogic { 23 | return &PBKDF2{ 24 | hashFactory: sha256.New, 25 | iterations: 20000, 26 | } 27 | }, 28 | }) 29 | } 30 | 31 | // PBKDF2 implements the cryptographic interface for PBKDF2 password derivation. 32 | type PBKDF2 struct { 33 | tools.ToolLogicBase 34 | hashFactory func() hash.Hash 35 | iterations int 36 | } 37 | 38 | // DeriveKeyFromPassword implements the ToolLogic interface. 39 | func (pd *PBKDF2) DeriveKeyFromPassword(password []byte, salt []byte) ([]byte, error) { 40 | return pbkdf2.Key( 41 | password, 42 | salt, 43 | pd.iterations, 44 | pd.Helper().DefaultSymmetricKeySize(), 45 | pd.hashFactory, 46 | ), nil 47 | } 48 | -------------------------------------------------------------------------------- /tools/gostdlib/poly1305.go: -------------------------------------------------------------------------------- 1 | package gostdlib 2 | 3 | import ( 4 | "errors" 5 | 6 | "golang.org/x/crypto/poly1305" //nolint:staticcheck // TODO: replace with newer package 7 | 8 | "github.com/safing/jess/tools" 9 | ) 10 | 11 | func init() { 12 | tools.Register(&tools.Tool{ 13 | Info: &tools.ToolInfo{ 14 | Name: "POLY1305", 15 | Purpose: tools.PurposeMAC, 16 | Options: []uint8{tools.OptionHasState}, 17 | KeySize: 32, 18 | SecurityLevel: 128, // TODO: do some more research 19 | Comment: "RFC 7539", 20 | Author: "Daniel J. Bernstein, 2005", 21 | }, 22 | Factory: func() tools.ToolLogic { return &Poly1305{} }, 23 | }) 24 | } 25 | 26 | // Poly1305 implements the cryptographic interface for Poly1305 message authentication codes. 27 | type Poly1305 struct { 28 | tools.ToolLogicBase 29 | key [32]byte 30 | keyIsSetUp bool 31 | keyUsed bool 32 | } 33 | 34 | // Setup implements the ToolLogic interface. 35 | func (poly *Poly1305) Setup() (err error) { 36 | // get key 37 | err = poly.Helper().FillNewSessionKey(poly.key[:]) 38 | if err != nil { 39 | return err 40 | } 41 | poly.keyIsSetUp = true 42 | 43 | return nil 44 | } 45 | 46 | // Reset implements the ToolLogic interface. 47 | func (poly *Poly1305) Reset() error { 48 | // clean up key 49 | poly.Helper().Burn(poly.key[:]) 50 | poly.keyUsed = false 51 | poly.keyIsSetUp = false 52 | 53 | return nil 54 | } 55 | 56 | // MAC implements the ToolLogic interface. 57 | func (poly *Poly1305) MAC(data, associatedData []byte) ([]byte, error) { 58 | // check for key initialization 59 | if !poly.keyIsSetUp { 60 | return nil, errors.New("key not initialized") 61 | } 62 | // check for key reuse 63 | if poly.keyUsed { 64 | return nil, errors.New("key reuse detected") 65 | } 66 | 67 | // create MAC 68 | mac := poly1305.New(&poly.key) 69 | poly.keyUsed = true 70 | // write data 71 | n, err := mac.Write(data) 72 | if err != nil { 73 | return nil, err 74 | } 75 | if n != len(data) { 76 | return nil, errors.New("failed to fully write data to Poly1305 MAC") 77 | } 78 | // write associated data 79 | if len(associatedData) > 0 { 80 | n, err := mac.Write(associatedData) 81 | if err != nil { 82 | return nil, err 83 | } 84 | if n != len(associatedData) { 85 | return nil, errors.New("failed to fully write associated data to Poly1305 MAC") 86 | } 87 | } 88 | 89 | // return sum 90 | return mac.Sum(nil), nil 91 | } 92 | -------------------------------------------------------------------------------- /tools/gostdlib/rsa-oaep.go: -------------------------------------------------------------------------------- 1 | package gostdlib 2 | 3 | import ( 4 | "crypto/rsa" 5 | "fmt" 6 | 7 | "github.com/safing/jess/tools" 8 | ) 9 | 10 | func init() { 11 | tools.Register(&tools.Tool{ 12 | Info: &tools.ToolInfo{ 13 | Name: "RSA-OAEP", 14 | Purpose: tools.PurposeKeyEncapsulation, 15 | Options: []uint8{ 16 | tools.OptionNeedsDedicatedHasher, 17 | tools.OptionNeedsSecurityLevel, 18 | }, 19 | Comment: "", // TODO 20 | Author: "", // TODO 21 | }, 22 | Factory: func() tools.ToolLogic { return &RsaOAEP{} }, 23 | }) 24 | } 25 | 26 | // RsaOAEP implements the cryptographic interface for RSA OAEP encryption. 27 | type RsaOAEP struct { 28 | rsaBase 29 | } 30 | 31 | // EncapsulateKey implements the ToolLogic interface. 32 | func (oaep *RsaOAEP) EncapsulateKey(key []byte, signet tools.SignetInt) ([]byte, error) { 33 | // transform public key 34 | rsaPubKey, ok := signet.PublicKey().(*rsa.PublicKey) 35 | if !ok { 36 | return nil, tools.ErrInvalidKey 37 | } 38 | if rsaPubKey == nil { 39 | return nil, tools.ErrInvalidKey 40 | } 41 | 42 | // check key length: The message must be no longer than the length of the public modulus minus twice the hash length, minus a further 2. 43 | maxMsgSize := rsaPubKey.Size() - (2 * oaep.HashTool().DigestSize) - 2 44 | if len(key) > maxMsgSize { 45 | return nil, fmt.Errorf( 46 | "key too long for encapsulation (rsa key would need to be at least %d bits in size to hold a key of %d bytes)", 47 | maxMsgSize*8, 48 | len(key), 49 | ) 50 | } 51 | 52 | return rsa.EncryptOAEP( 53 | oaep.HashTool().New(), 54 | oaep.Helper().Random(), 55 | rsaPubKey, 56 | key, 57 | nil, // label 58 | ) 59 | } 60 | 61 | // UnwrapKey implements the ToolLogic interface. 62 | func (oaep *RsaOAEP) UnwrapKey(wrappedKey []byte, signet tools.SignetInt) ([]byte, error) { 63 | rsaPrivKey, ok := signet.PrivateKey().(*rsa.PrivateKey) 64 | if !ok { 65 | return nil, tools.ErrInvalidKey 66 | } 67 | 68 | return rsa.DecryptOAEP( 69 | oaep.HashTool().New(), 70 | oaep.Helper().Random(), 71 | rsaPrivKey, 72 | wrappedKey, 73 | nil, // label 74 | ) 75 | } 76 | -------------------------------------------------------------------------------- /tools/gostdlib/rsa-pss.go: -------------------------------------------------------------------------------- 1 | package gostdlib 2 | 3 | import ( 4 | "crypto/rsa" 5 | "errors" 6 | 7 | "github.com/safing/jess/tools" 8 | ) 9 | 10 | func init() { 11 | tools.Register(&tools.Tool{ 12 | Info: &tools.ToolInfo{ 13 | Name: "RSA-PSS", 14 | Purpose: tools.PurposeSigning, 15 | Options: []uint8{ 16 | tools.OptionNeedsManagedHasher, 17 | tools.OptionNeedsSecurityLevel, 18 | }, 19 | Comment: "RFC 8017", 20 | Author: "Mihir Bellare, Phillip Rogaway, 1998", 21 | }, 22 | Factory: func() tools.ToolLogic { return &RsaPSS{} }, 23 | }) 24 | } 25 | 26 | // RsaPSS implements the cryptographic interface for RSA PSS signatures. 27 | type RsaPSS struct { 28 | rsaBase 29 | } 30 | 31 | // Sign implements the ToolLogic interface. 32 | func (pss *RsaPSS) Sign(data, associatedData []byte, signet tools.SignetInt) ([]byte, error) { 33 | rsaPrivKey, ok := signet.PrivateKey().(*rsa.PrivateKey) 34 | if !ok { 35 | return nil, tools.ErrInvalidKey 36 | } 37 | 38 | hashsum, err := pss.ManagedHashSum() 39 | if err != nil { 40 | return nil, err 41 | } 42 | if pss.HashTool().CryptoHashID == 0 { 43 | return nil, errors.New("tool PSS is only compatible with Golang crypto.Hash hash functions") 44 | } 45 | 46 | return rsa.SignPSS( 47 | pss.Helper().Random(), 48 | rsaPrivKey, 49 | pss.HashTool().CryptoHashID, 50 | hashsum, 51 | nil, // *rsa.PSSOptions 52 | ) 53 | } 54 | 55 | // Verify implements the ToolLogic interface. 56 | func (pss *RsaPSS) Verify(data, associatedData, signature []byte, signet tools.SignetInt) error { 57 | rsaPubKey, ok := signet.PublicKey().(*rsa.PublicKey) 58 | if !ok { 59 | return tools.ErrInvalidKey 60 | } 61 | 62 | hashsum, err := pss.ManagedHashSum() 63 | if err != nil { 64 | return err 65 | } 66 | if pss.HashTool().CryptoHashID == 0 { 67 | return errors.New("tool PSS is only compatible with Golang crypto.Hash hash functions") 68 | } 69 | 70 | return rsa.VerifyPSS( 71 | rsaPubKey, 72 | pss.HashTool().CryptoHashID, 73 | hashsum, 74 | signature, 75 | nil, // *rsa.PSSOptions 76 | ) 77 | } 78 | -------------------------------------------------------------------------------- /tools/gostdlib/salsa20.go: -------------------------------------------------------------------------------- 1 | package gostdlib 2 | 3 | import ( 4 | "golang.org/x/crypto/salsa20" 5 | 6 | "github.com/safing/jess/tools" 7 | ) 8 | 9 | func init() { 10 | tools.Register(&tools.Tool{ 11 | Info: &tools.ToolInfo{ 12 | Name: "SALSA20", 13 | Purpose: tools.PurposeCipher, 14 | Options: []uint8{tools.OptionHasState}, 15 | KeySize: 32, // 265 bits 16 | NonceSize: 8, // 64 bits 17 | SecurityLevel: 256, 18 | Comment: "", 19 | Author: "Daniel J. Bernstein, 2007", 20 | }, 21 | Factory: func() tools.ToolLogic { return &Salsa20{} }, 22 | }) 23 | } 24 | 25 | // Salsa20 implements the cryptographic interface for Salsa20 encryption. 26 | type Salsa20 struct { 27 | tools.ToolLogicBase 28 | key [32]byte 29 | nonce []byte 30 | } 31 | 32 | // Setup implements the ToolLogic interface. 33 | func (salsa *Salsa20) Setup() (err error) { 34 | // get key 35 | err = salsa.Helper().FillNewSessionKey(salsa.key[:]) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | // get nonce 41 | salsa.nonce, err = salsa.Helper().NewSessionNonce() 42 | if err != nil { 43 | return err 44 | } 45 | 46 | return nil 47 | } 48 | 49 | // Reset implements the ToolLogic interface. 50 | func (salsa *Salsa20) Reset() error { 51 | // clean up keys 52 | salsa.Helper().Burn(salsa.key[:]) 53 | salsa.Helper().Burn(salsa.nonce) 54 | 55 | return nil 56 | } 57 | 58 | // Encrypt implements the ToolLogic interface. 59 | func (salsa *Salsa20) Encrypt(data []byte) ([]byte, error) { 60 | // encrypt 61 | salsa20.XORKeyStream(data, data, salsa.nonce, &salsa.key) 62 | 63 | return data, nil 64 | } 65 | 66 | // Decrypt implements the ToolLogic interface. 67 | func (salsa *Salsa20) Decrypt(data []byte) ([]byte, error) { 68 | // decrypt 69 | salsa20.XORKeyStream(data, data, salsa.nonce, &salsa.key) 70 | 71 | return data, nil 72 | } 73 | -------------------------------------------------------------------------------- /tools/gostdlib/scrypt.go: -------------------------------------------------------------------------------- 1 | package gostdlib 2 | 3 | import ( 4 | "golang.org/x/crypto/scrypt" 5 | 6 | "github.com/safing/jess/tools" 7 | ) 8 | 9 | func init() { 10 | tools.Register(&tools.Tool{ 11 | Info: &tools.ToolInfo{ 12 | Name: "SCRYPT-20", 13 | Purpose: tools.PurposePassDerivation, 14 | Options: []uint8{tools.OptionNeedsDefaultKeySize}, 15 | SecurityLevel: 0, // security of default key size 16 | Comment: "", 17 | Author: "Colin Percival, 2009", 18 | }, 19 | Factory: func() tools.ToolLogic { 20 | return &SCRYPT{ 21 | n: 1 << 20, // 2^20 resp. 1,048,576 - CPU/memory cost parameter 22 | r: 8, // The blocksize parameter 23 | p: 1, // Parallelization parameter 24 | } 25 | }, 26 | }) 27 | } 28 | 29 | // SCRYPT implements the cryptographic interface for SCRYPT password derivation. 30 | type SCRYPT struct { 31 | tools.ToolLogicBase 32 | n int // CPU/memory cost parameter 33 | r int // The blocksize parameter 34 | p int // Parallelization parameter 35 | } 36 | 37 | // DeriveKeyFromPassword implements the ToolLogic interface. 38 | func (sc *SCRYPT) DeriveKeyFromPassword(password []byte, salt []byte) ([]byte, error) { 39 | return scrypt.Key(password, salt, sc.n, sc.r, sc.p, sc.Helper().DefaultSymmetricKeySize()) 40 | } 41 | -------------------------------------------------------------------------------- /tools/interfaces.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "crypto" 5 | "io" 6 | ) 7 | 8 | // HelperInt is an interface to Helper. 9 | type HelperInt interface { 10 | // NewSessionKey returns a new session key (or nonce) in tool's specified length. 11 | NewSessionKey() ([]byte, error) 12 | 13 | // FillNewSessionKey fills the given []byte slice with a new session key (or nonce). 14 | FillNewSessionKey(key []byte) error 15 | 16 | // NewSessionNonce returns a new session nonce in tool's specified length. 17 | NewSessionNonce() ([]byte, error) 18 | 19 | // Random returns the io.Reader for reading randomness. 20 | Random() io.Reader 21 | 22 | // RandomBytes returns the specified amount of random bytes in a []byte slice. 23 | RandomBytes(n int) ([]byte, error) 24 | 25 | // Burn gets rid of the given []byte slice(s). This is currently ineffective, see known issues in the project's README. 26 | Burn(data ...[]byte) 27 | 28 | // DefaultSymmetricKeySize returns the default key size for this session. 29 | DefaultSymmetricKeySize() int 30 | 31 | // SecurityLevel returns the effective (ie. lowest) security level for this session. 32 | SecurityLevel() int 33 | 34 | // MaxSecurityLevel returns the (highest) security level for this session. 35 | MaxSecurityLevel() int 36 | } 37 | 38 | // SignetInt is a minimal interface to Signet. 39 | type SignetInt interface { 40 | // GetStoredKey returns the stored key and whether it is public. 41 | GetStoredKey() (key []byte, public bool) 42 | 43 | // SetStoredKey sets a new stored key and whether it is public. 44 | SetStoredKey(newKey []byte, public bool) 45 | 46 | // PublicKey returns the public key. 47 | PublicKey() crypto.PublicKey 48 | 49 | // PrivateKey returns the private key or nil, if there is none. 50 | PrivateKey() crypto.PrivateKey 51 | 52 | // SetLoadedKeys sets the loaded public and private keys. 53 | SetLoadedKeys(pubKey crypto.PublicKey, privKey crypto.PrivateKey) 54 | 55 | // LoadKey loads the serialized key pair. 56 | LoadKey() error 57 | } 58 | -------------------------------------------------------------------------------- /tools/tools.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | ) 7 | 8 | var ( 9 | toolMap = make(map[string]*Tool) 10 | toolList = sortableToolList{} 11 | ) 12 | 13 | // Register registers a new Tool. This function may only be called in init() functions. 14 | func Register(tool *Tool) { 15 | // register in lists 16 | toolMap[tool.Info.Name] = tool 17 | toolList = append(toolList, tool) 18 | sort.Sort(toolList) 19 | } 20 | 21 | // Get returns the Tool with the given name. 22 | func Get(name string) (*Tool, error) { 23 | tool, ok := toolMap[name] 24 | if !ok { 25 | return nil, fmt.Errorf("Tool %s %w", name, ErrNotFound) 26 | } 27 | return tool, nil 28 | } 29 | 30 | // New returns a new instance of a Tool's Logic with the given name. 31 | func New(name string) (ToolLogic, error) { 32 | tool, err := Get(name) 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | return tool.Factory(), nil 38 | } 39 | 40 | // AsMap returns all Tools in a map. The returned map must not be modified. 41 | func AsMap() map[string]*Tool { 42 | return toolMap 43 | } 44 | 45 | // AsList returns all Tools in a slice. The returned slice must not be modified. 46 | func AsList() []*Tool { 47 | return toolList 48 | } 49 | 50 | type sortableToolList []*Tool 51 | 52 | // Len implements sort.Interface. 53 | func (l sortableToolList) Len() int { return len(l) } 54 | 55 | // Swap implements sort.Interface. 56 | func (l sortableToolList) Swap(i, j int) { l[i], l[j] = l[j], l[i] } 57 | 58 | // Less implements sort.Interface. 59 | func (l sortableToolList) Less(i, j int) bool { return l[i].Info.Name < l[j].Info.Name } 60 | -------------------------------------------------------------------------------- /truststore.go: -------------------------------------------------------------------------------- 1 | package jess 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "sync" 7 | ) 8 | 9 | // TrustStore filter options. 10 | const ( 11 | FilterAny uint8 = iota 12 | FilterSignetOnly 13 | FilterRecipientOnly 14 | ) 15 | 16 | // TrustStore errors. 17 | var ( 18 | ErrSignetNotFound = errors.New("could not find signet") 19 | ErrEnvelopeNotFound = errors.New("could not find envelope") 20 | ) 21 | 22 | // TrustStore holds a set of trusted Signets and Recipients. 23 | type TrustStore interface { 24 | // GetSignet returns the Signet with the given ID. 25 | GetSignet(id string, recipient bool) (*Signet, error) 26 | } 27 | 28 | // MemTrustStore is a simple trust store using a Go map as backend. 29 | type MemTrustStore struct { 30 | lock sync.Mutex 31 | storage map[string]*Signet 32 | } 33 | 34 | // GetSignet returns the Signet with the given ID. 35 | func (mts *MemTrustStore) GetSignet(id string, recipient bool) (*Signet, error) { 36 | mts.lock.Lock() 37 | defer mts.lock.Unlock() 38 | 39 | // get from storage 40 | signet, ok := mts.storage[makeStorageID(id, recipient)] 41 | if !ok { 42 | return nil, ErrSignetNotFound 43 | } 44 | 45 | return signet, nil 46 | } 47 | 48 | // StoreSignet stores a Signet in the TrustStore. 49 | func (mts *MemTrustStore) StoreSignet(signet *Signet) error { 50 | // check for ID 51 | if signet.ID == "" { 52 | return errors.New("signets require an ID to be stored in a trust store") 53 | } 54 | 55 | mts.lock.Lock() 56 | defer mts.lock.Unlock() 57 | 58 | // store 59 | mts.storage[makeStorageID(signet.ID, signet.Public)] = signet 60 | return nil 61 | } 62 | 63 | // DeleteSignet deletes the Signet or Recipient with the given ID. 64 | func (mts *MemTrustStore) DeleteSignet(id string, recipient bool) error { 65 | mts.lock.Lock() 66 | defer mts.lock.Unlock() 67 | 68 | // delete 69 | delete(mts.storage, makeStorageID(id, recipient)) 70 | return nil 71 | } 72 | 73 | // SelectSignets returns a selection of the signets in the trust store. Results are filtered by tool/algorithm and whether it you're looking for a signet (private key) or a recipient (public key). 74 | func (mts *MemTrustStore) SelectSignets(filter uint8, schemes ...string) ([]*Signet, error) { 75 | mts.lock.Lock() 76 | defer mts.lock.Unlock() 77 | 78 | var selection []*Signet //nolint:prealloc 79 | for _, signet := range mts.storage { 80 | // check signet scheme 81 | if len(schemes) > 0 && !stringInSlice(signet.Scheme, schemes) { 82 | return nil, nil 83 | } 84 | 85 | // check type filter 86 | switch filter { 87 | case FilterSignetOnly: 88 | if signet.Public { 89 | continue 90 | } 91 | case FilterRecipientOnly: 92 | if !signet.Public { 93 | continue 94 | } 95 | } 96 | 97 | selection = append(selection, signet) 98 | } 99 | 100 | return selection, nil 101 | } 102 | 103 | // NewMemTrustStore returns a new in-memory TrustStore. 104 | func NewMemTrustStore() *MemTrustStore { 105 | return &MemTrustStore{ 106 | storage: make(map[string]*Signet), 107 | } 108 | } 109 | 110 | func makeStorageID(id string, recipient bool) string { 111 | if recipient { 112 | return fmt.Sprintf("%s.recipient", id) 113 | } 114 | return fmt.Sprintf("%s.signet", id) 115 | } 116 | 117 | func stringInSlice(s string, a []string) bool { 118 | for _, entry := range a { 119 | if entry == s { 120 | return true 121 | } 122 | } 123 | return false 124 | } 125 | -------------------------------------------------------------------------------- /truststores/dir_test.go: -------------------------------------------------------------------------------- 1 | package truststores 2 | 3 | func init() { 4 | // interface compliance test 5 | var testDirTrustStore ExtendedTrustStore 6 | testDirTrustStore, _ = NewDirTrustStore("/tmp") 7 | _ = testDirTrustStore 8 | } 9 | -------------------------------------------------------------------------------- /truststores/extended.go: -------------------------------------------------------------------------------- 1 | package truststores 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/safing/jess" 7 | ) 8 | 9 | // ErrNotSupportedByTrustStore is returned by trust stores if they do not 10 | // support certain actions. 11 | var ErrNotSupportedByTrustStore = errors.New("action not supported by trust store") 12 | 13 | // ExtendedTrustStore holds a set of trusted Signets, Recipients and Envelopes. 14 | type ExtendedTrustStore interface { 15 | jess.TrustStore 16 | 17 | // GetSignet returns the Signet with the given ID. 18 | // GetSignet(id string, recipient bool) (*Signet, error) 19 | 20 | // StoreSignet stores a Signet. 21 | StoreSignet(signet *jess.Signet) error 22 | 23 | // DeleteSignet deletes the Signet or Recipient with the given ID. 24 | DeleteSignet(id string, recipient bool) error 25 | 26 | // SelectSignets returns a selection of the signets in the trust store. Results are filtered by tool/algorithm and whether it you're looking for a signet (private key) or a recipient (public key). 27 | SelectSignets(filter uint8, schemes ...string) ([]*jess.Signet, error) 28 | 29 | // GetEnvelope returns the Envelope with the given name. 30 | GetEnvelope(name string) (*jess.Envelope, error) 31 | 32 | // StoreEnvelope stores an Envelope. 33 | StoreEnvelope(envelope *jess.Envelope) error 34 | 35 | // DeleteEnvelope deletes the Envelope with the given name. 36 | DeleteEnvelope(name string) error 37 | 38 | // AllEnvelopes returns all envelopes. 39 | AllEnvelopes() ([]*jess.Envelope, error) 40 | } 41 | -------------------------------------------------------------------------------- /truststores/io.go: -------------------------------------------------------------------------------- 1 | package truststores 2 | 3 | import ( 4 | "errors" 5 | "io/fs" 6 | "os" 7 | 8 | "github.com/safing/jess" 9 | "github.com/safing/structures/dsd" 10 | ) 11 | 12 | // WriteSignetToFile serializes the signet and writes it to the given file. 13 | func WriteSignetToFile(signet *jess.Signet, filename string) error { 14 | // check ID 15 | if signet.ID == "" { 16 | return errors.New("signets require an ID to be stored in a trust store") 17 | } 18 | ok := NamePlaysNiceWithFS(signet.ID) 19 | if !ok { 20 | return errInvalidSignetIDChars 21 | } 22 | 23 | // serialize 24 | data, err := dsd.DumpIndent(signet, dsd.JSON, "\t") 25 | if err != nil { 26 | return err 27 | } 28 | 29 | // write 30 | err = os.WriteFile(filename, data, 0o0600) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | return nil 36 | } 37 | 38 | // LoadSignetFromFile loads a signet from the given filepath. 39 | func LoadSignetFromFile(filename string) (*jess.Signet, error) { 40 | data, err := os.ReadFile(filename) 41 | if err != nil { 42 | if errors.Is(err, fs.ErrNotExist) { 43 | return nil, jess.ErrSignetNotFound 44 | } 45 | return nil, err 46 | } 47 | 48 | signet := &jess.Signet{} 49 | _, err = dsd.Load(data, signet) 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | return signet, nil 55 | } 56 | 57 | // WriteEnvelopeToFile serializes the envelope and writes it to the given file. 58 | func WriteEnvelopeToFile(envelope *jess.Envelope, filename string) error { 59 | // check name 60 | if envelope.Name == "" { 61 | return errors.New("envelopes require a name to be stored in a trust store") 62 | } 63 | ok := NamePlaysNiceWithFS(envelope.Name) 64 | if !ok { 65 | return errInvalidEnvelopeNameChars 66 | } 67 | 68 | // serialize 69 | data, err := dsd.DumpIndent(envelope, dsd.JSON, "\t") 70 | if err != nil { 71 | return err 72 | } 73 | 74 | // write to storage 75 | err = os.WriteFile(filename, data, 0600) //nolint:gofumpt // gofumpt is ignorant of octal numbers. 76 | if err != nil { 77 | return err 78 | } 79 | 80 | return nil 81 | } 82 | 83 | // LoadEnvelopeFromFile loads an envelope from the given filepath. 84 | func LoadEnvelopeFromFile(filename string) (*jess.Envelope, error) { 85 | data, err := os.ReadFile(filename) 86 | if err != nil { 87 | if errors.Is(err, fs.ErrNotExist) { 88 | return nil, jess.ErrEnvelopeNotFound 89 | } 90 | return nil, err 91 | } 92 | 93 | // load envelope 94 | envelope := &jess.Envelope{} 95 | _, err = dsd.Load(data, envelope) 96 | if err != nil { 97 | return nil, err 98 | } 99 | 100 | // load suite using SuiteID 101 | err = envelope.LoadSuite() 102 | if err != nil { 103 | return nil, err 104 | } 105 | 106 | return envelope, nil 107 | } 108 | -------------------------------------------------------------------------------- /truststores/keyring.go: -------------------------------------------------------------------------------- 1 | package truststores 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/zalando/go-keyring" 8 | 9 | "github.com/safing/jess" 10 | ) 11 | 12 | const ( 13 | keyringServiceNamePrefix = "jess:" 14 | 15 | keyringSelfcheckKey = "_selfcheck" 16 | keyringSelfcheckValue = "!selfcheck" 17 | ) 18 | 19 | // KeyringTrustStore is a trust store that uses the system keyring. 20 | // It does not support listing entries, so it cannot be easily managed. 21 | type KeyringTrustStore struct { 22 | serviceName string 23 | } 24 | 25 | // NewKeyringTrustStore returns a new keyring trust store with the given service name. 26 | // The effect of the service name depends on the operating system. 27 | // Read more at https://pkg.go.dev/github.com/zalando/go-keyring 28 | func NewKeyringTrustStore(serviceName string) (*KeyringTrustStore, error) { 29 | krts := &KeyringTrustStore{ 30 | serviceName: keyringServiceNamePrefix + serviceName, 31 | } 32 | 33 | // Run a self-check. 34 | err := keyring.Set(krts.serviceName, keyringSelfcheckKey, keyringSelfcheckValue) 35 | if err != nil { 36 | return nil, err 37 | } 38 | selfcheckReturn, err := keyring.Get(krts.serviceName, keyringSelfcheckKey) 39 | if err != nil { 40 | return nil, err 41 | } 42 | if selfcheckReturn != keyringSelfcheckValue { 43 | return nil, errors.New("keyring is faulty") 44 | } 45 | 46 | return krts, nil 47 | } 48 | 49 | // GetSignet returns the Signet with the given ID. 50 | func (krts *KeyringTrustStore) GetSignet(id string, recipient bool) (*jess.Signet, error) { 51 | // Build ID. 52 | if recipient { 53 | id += recipientSuffix 54 | } else { 55 | id += signetSuffix 56 | } 57 | 58 | // Get data from keyring. 59 | data, err := keyring.Get(krts.serviceName, id) 60 | if err != nil { 61 | return nil, fmt.Errorf("%w: %w", jess.ErrSignetNotFound, err) 62 | } 63 | 64 | // Parse and return. 65 | return jess.SignetFromBase58(data) 66 | } 67 | 68 | // StoreSignet stores a Signet. 69 | func (krts *KeyringTrustStore) StoreSignet(signet *jess.Signet) error { 70 | // Build ID. 71 | var id string 72 | if signet.Public { 73 | id = signet.ID + recipientSuffix 74 | } else { 75 | id = signet.ID + signetSuffix 76 | } 77 | 78 | // Serialize. 79 | data, err := signet.ToBase58() 80 | if err != nil { 81 | return err 82 | } 83 | 84 | // Save to keyring. 85 | return keyring.Set(krts.serviceName, id, data) 86 | } 87 | 88 | // DeleteSignet deletes the Signet or Recipient with the given ID. 89 | func (krts *KeyringTrustStore) DeleteSignet(id string, recipient bool) error { 90 | // Build ID. 91 | if recipient { 92 | id += recipientSuffix 93 | } else { 94 | id += signetSuffix 95 | } 96 | 97 | // Delete from keyring. 98 | return keyring.Delete(krts.serviceName, id) 99 | } 100 | 101 | // SelectSignets returns a selection of the signets in the trust store. Results are filtered by tool/algorithm and whether it you're looking for a signet (private key) or a recipient (public key). 102 | func (krts *KeyringTrustStore) SelectSignets(filter uint8, schemes ...string) ([]*jess.Signet, error) { 103 | return nil, ErrNotSupportedByTrustStore 104 | } 105 | 106 | // GetEnvelope returns the Envelope with the given name. 107 | func (krts *KeyringTrustStore) GetEnvelope(name string) (*jess.Envelope, error) { 108 | // Build ID. 109 | name += envelopeSuffix 110 | 111 | // Get data from keyring. 112 | data, err := keyring.Get(krts.serviceName, name) 113 | if err != nil { 114 | return nil, fmt.Errorf("%w: %w", jess.ErrEnvelopeNotFound, err) 115 | } 116 | 117 | // Parse and return. 118 | return jess.EnvelopeFromBase58(data) 119 | } 120 | 121 | // StoreEnvelope stores an Envelope. 122 | func (krts *KeyringTrustStore) StoreEnvelope(envelope *jess.Envelope) error { 123 | // Build ID. 124 | name := envelope.Name + envelopeSuffix 125 | 126 | // Serialize. 127 | data, err := envelope.ToBase58() 128 | if err != nil { 129 | return err 130 | } 131 | 132 | // Save to keyring. 133 | return keyring.Set(krts.serviceName, name, data) 134 | } 135 | 136 | // DeleteEnvelope deletes the Envelope with the given name. 137 | func (krts *KeyringTrustStore) DeleteEnvelope(name string) error { 138 | // Build ID. 139 | name += envelopeSuffix 140 | 141 | // Delete from keyring. 142 | return keyring.Delete(krts.serviceName, name) 143 | } 144 | 145 | // AllEnvelopes returns all envelopes. 146 | func (krts *KeyringTrustStore) AllEnvelopes() ([]*jess.Envelope, error) { 147 | return nil, ErrNotSupportedByTrustStore 148 | } 149 | -------------------------------------------------------------------------------- /truststores/utils.go: -------------------------------------------------------------------------------- 1 | package truststores 2 | 3 | func stringInSlice(s string, a []string) bool { 4 | for _, entry := range a { 5 | if entry == s { 6 | return true 7 | } 8 | } 9 | return false 10 | } 11 | --------------------------------------------------------------------------------