diff --git a/.github/actions/build-gosop/action.yml b/.github/actions/build-gosop/action.yml index 8c890844..a754b676 100644 --- a/.github/actions/build-gosop/action.yml +++ b/.github/actions/build-gosop/action.yml @@ -2,7 +2,6 @@ name: 'build-gosop' description: 'Build gosop from the current branch' inputs: - gopenpgp-ref: description: 'gopenpgp branch tag or commit to build from' required: true @@ -21,6 +20,12 @@ runs: with: ref: ${{ inputs.gopenpgp-ref }} path: gopenpgp + - name: Set env + run: echo "GOSOP_BRANCH_REF=$(./.github/test-suite/determine_gosop_branch.sh)" >> $GITHUB_ENV + shell: bash + - name: Print gosop branch + run: echo ${{ env.GOSOP_BRANCH_REF}} + shell: bash # Build gosop - name: Set up latest golang uses: actions/setup-go@v3 @@ -30,6 +35,7 @@ runs: uses: actions/checkout@v3 with: repository: ProtonMail/gosop + ref: ${{ env.GOSOP_BRANCH_REF}} path: gosop - name: Cache go modules uses: actions/cache@v3 diff --git a/.github/test-suite/build_gosop.sh b/.github/test-suite/build_gosop.sh index e19623e2..dc5298a6 100755 --- a/.github/test-suite/build_gosop.sh +++ b/.github/test-suite/build_gosop.sh @@ -1,4 +1,6 @@ +VERSION=$(awk '/^module github.com\/ProtonMail\/gopenpgp\/v[0-9]+/ {print $NF}' gopenpgp/go.mod | awk -F'v' '{print $2}') + cd gosop -echo "replace github.com/ProtonMail/gopenpgp/v2 => ../gopenpgp" >> go.mod -go get github.com/ProtonMail/gopenpgp/v2/crypto +echo "replace github.com/ProtonMail/gopenpgp/v${VERSION} => ../gopenpgp" >> go.mod +go get github.com/ProtonMail/gopenpgp/v${VERSION}/crypto go build . diff --git a/.github/test-suite/config.json.template b/.github/test-suite/config.json.template index 2e491979..4d67575a 100644 --- a/.github/test-suite/config.json.template +++ b/.github/test-suite/config.json.template @@ -5,8 +5,8 @@ "path": "__GOSOP_BRANCH__" }, { - "id": "gosop-main", - "path": "__GOSOP_MAIN__" + "id": "gosop-target", + "path": "__GOSOP_TARGET__" }, { "path": "__SQOP__" @@ -17,10 +17,6 @@ { "path": "__SOP_OPENPGPJS__" }, - { - "id": "gosop-v2", - "path": "__GOSOP_V2__" - }, { "path": "__RNP_SOP__" } diff --git a/.github/test-suite/determine_gosop_branch.sh b/.github/test-suite/determine_gosop_branch.sh new file mode 100755 index 00000000..d3525783 --- /dev/null +++ b/.github/test-suite/determine_gosop_branch.sh @@ -0,0 +1,7 @@ +VERSION=$(awk '/^module github.com\/ProtonMail\/gopenpgp\/v[0-9]+/ {print $NF}' gopenpgp/go.mod | awk -F'v' '{print $2}') + +if [ "$VERSION" -eq 3 ]; then + echo "gosop-gopenpgp-v3" +else + echo "main" +fi diff --git a/.github/test-suite/prepare_config.sh b/.github/test-suite/prepare_config.sh index ca3feefc..e18d8031 100755 --- a/.github/test-suite/prepare_config.sh +++ b/.github/test-suite/prepare_config.sh @@ -1,13 +1,12 @@ CONFIG_TEMPLATE=$1 CONFIG_OUTPUT=$2 GOSOP_BRANCH=$3 -GOSOP_MAIN=$4 +GOSOP_TARGET=$4 cat $CONFIG_TEMPLATE \ | sed "s@__GOSOP_BRANCH__@${GOSOP_BRANCH}@g" \ - | sed "s@__GOSOP_MAIN__@${GOSOP_MAIN}@g" \ + | sed "s@__GOSOP_TARGET__@${GOSOP_TARGET}@g" \ | sed "s@__SQOP__@${SQOP}@g" \ | sed "s@__GPGME_SOP__@${GPGME_SOP}@g" \ - | sed "s@__SOP_OPENPGPJS__@${SOP_OPENPGPJS}@g" \ - | sed "s@__GOSOP_V2__@${GOSOP_DIR_V2}/gosop@g" \ + | sed "s@__SOP_OPENPGPJS__@${SOP_OPENPGPJS_V2}@g" \ | sed "s@__RNP_SOP__@${RNP_SOP}@g" \ > $CONFIG_OUTPUT \ No newline at end of file diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 5cf18b71..f0cb4484 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -4,12 +4,12 @@ on: push: branches: [ main ] pull_request: - branches: [ main ] + branches: [ main, v3 ] jobs: build: name: Build library for Android with gomobile - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Set up JDK 1.8 diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 8f945325..af48ce75 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -4,7 +4,7 @@ on: push: branches: [ main ] pull_request: - branches: [ main ] + branches: [ main, v3 ] jobs: test: @@ -36,7 +36,7 @@ jobs: - name: Test run: go test -v -race ./... - + lint: name: Lint runs-on: ubuntu-latest @@ -48,4 +48,4 @@ jobs: - name: golangci-lint uses: golangci/golangci-lint-action@v3 with: - version: v1.50.1 \ No newline at end of file + version: v1.54.2 \ No newline at end of file diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index 2e2b6e97..9385f4df 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -4,7 +4,7 @@ on: push: branches: [ main ] pull_request: - branches: [ main ] + branches: [ main, v3 ] jobs: build: @@ -12,10 +12,10 @@ jobs: runs-on: macos-latest steps: - - name: Set up xcode 14.2 + - name: Set up xcode 14.3 uses: maxim-lobanov/setup-xcode@v1 with: - xcode-version: 14.2 + xcode-version: 14.3 id: xcode - name: Set up Go 1.x @@ -31,9 +31,6 @@ jobs: env: platform: ${{ 'iOS Simulator' }} run: | - for d in $ANDROID_NDK_HOME/../23*; do - ANDROID_NDK_HOME=$d - done ./build.sh apple find dist diff --git a/.github/workflows/sop-test-suite.yml b/.github/workflows/sop-test-suite.yml index ca761827..6ed7aede 100644 --- a/.github/workflows/sop-test-suite.yml +++ b/.github/workflows/sop-test-suite.yml @@ -2,7 +2,7 @@ name: SOP interoperability test suite on: pull_request: - branches: [ main ] + branches: [ main, v3 ] jobs: @@ -23,8 +23,8 @@ jobs: name: gosop-${{ github.sha }} path: ./gosop-${{ github.sha }} - build-gosop-main: - name: Build gosop from main + build-gosop-target: + name: Build gosop from target runs-on: ubuntu-latest steps: - name: Checkout @@ -32,40 +32,40 @@ jobs: - name: Build gosop from branch uses: ./.github/actions/build-gosop with: - gopenpgp-ref: main - binary-location: ./gosop-main + gopenpgp-ref: ${{ github.base_ref }} + binary-location: ./gosop-target # Upload as artifact - - name: Upload gosop-main artifact + - name: Upload gosop-target artifact uses: actions/upload-artifact@v3 with: - name: gosop-main - path: ./gosop-main + name: gosop-target + path: ./gosop-target test-suite: name: Run interoperability test suite runs-on: ubuntu-latest container: - image: ghcr.io/protonmail/openpgp-interop-test-docker:v1.1.4 + image: ghcr.io/protonmail/openpgp-interop-test-docker:v1.1.7 credentials: username: ${{ github.actor }} password: ${{ secrets.github_token }} needs: - build-gosop - - build-gosop-main + - build-gosop-target steps: - name: Checkout uses: actions/checkout@v3 - # Fetch gosop from main - - name: Download gosop-main + # Fetch gosop from target + - name: Download gosop-target uses: actions/download-artifact@v3 with: - name: gosop-main - # Test gosop-main - - name: Make gosop-main executable - run: chmod +x gosop-main - - name: Print gosop-main version - run: ./gosop-main version --extended + name: gosop-target + # Test gosop-target + - name: Make gosop-target executable + run: chmod +x gosop-target + - name: Print gosop-target version + run: ./gosop-target version --extended # Fetch gosop from branch - name: Download gosop-branch uses: actions/download-artifact@v3 @@ -80,7 +80,7 @@ jobs: run: ./gosop-branch version --extended # Run test suite - name: Prepare test configuration - run: ./.github/test-suite/prepare_config.sh $CONFIG_TEMPLATE $CONFIG_OUTPUT $GITHUB_WORKSPACE/gosop-branch $GITHUB_WORKSPACE/gosop-main + run: ./.github/test-suite/prepare_config.sh $CONFIG_TEMPLATE $CONFIG_OUTPUT $GITHUB_WORKSPACE/gosop-branch $GITHUB_WORKSPACE/gosop-target env: CONFIG_TEMPLATE: .github/test-suite/config.json.template CONFIG_OUTPUT: .github/test-suite/config.json @@ -104,8 +104,8 @@ jobs: name: test-suite-results.html path: .github/test-suite/test-suite-results.html - compare-with-main: - name: Compare with main + compare-with-target: + name: Compare with target runs-on: ubuntu-latest needs: test-suite steps: @@ -117,9 +117,9 @@ jobs: with: name: test-suite-results.json - name: Compare with baseline - uses: ProtonMail/openpgp-interop-test-analyzer@5d7f4b6868ebe3bfc909302828342c461f5f4940 + uses: ProtonMail/openpgp-interop-test-analyzer@v2.0.0 with: results: ${{ steps.download-test-results.outputs.download-path }}/test-suite-results.json output: baseline-comparison.json - baseline: gosop-main + baseline: gosop-target target: gosop-branch diff --git a/.gitignore b/.gitignore index 33c4300f..e5e7e76c 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,4 @@ vendor *.html reports .idea -v2 \ No newline at end of file +v3 \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml index 9c9d1a5f..33f2e665 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -4,11 +4,11 @@ linters-settings: - BUG - FIXME funlen: - lines: 100 + lines: 150 statements: 80 cyclop: # the minimal code complexity to report - max-complexity: 20 + max-complexity: 26 gocognit: min-complexity: 45 @@ -18,6 +18,19 @@ issues: - Using the variable on range scope `tt` in function literal - GetJsonSHA256Fingerprints should be GetJSONSHA256Fingerprints - ST1003 # CamelCase variables; see constants/cipher.go + - missing output for example, go test can't validate it + - variable 'hasExpiredEntity' is only used in the if-statement + exclude-rules: + - path: crypto/key_clear.go + text: "SA1019" + - path: crypto/crypto_example_test.go + text: "G101: Potential hardcoded credentials" + - path: crypto/encrypt_decrypt_test.go + text: "Using the variable on range scope" + - path: crypto/encrypt_decrypt_err_test.go + text: "Using the variable on range scope" + - path: crypto/sign_verify_test.go + text: "Using the variable on range scope" linters: enable-all: true @@ -47,4 +60,6 @@ linters: - forcetypeassert # Forces to assert types in tests - nonamedreturns # Disallows named returns - exhaustruct # Forces all structs to be named - - nosnakecase # Disallows snake case \ No newline at end of file + - nosnakecase # Disallows snake case + - depguard + - nestif \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 5de4774c..4ce4f252 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,22 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [2.8.0-alpha.1] 2024-04-09 +## [3.0.0-alpha.4] 2024-07-18 +### Changed +- Update go-crypto to `1.1.0-alpha.4`. +- Remove logic to get a profile by name. +- Reduce preset profiles to `Default`, `RFC4880`, and `RFC9580`. +- Update go-crypto to check signature details of binding signatures. + +## [3.0.0-alpha.3] 2024-06-25 +### Added +- API to armor data with the option to remove the checksum + +### Changed +- All armor functions append a checksum per default for compatibility with certain libraries although the crypto-refresh advises not to. +- `Encryption` and `Sign` handle now append a checksum when armoring. If the produced OpenPGP packets are crypto-refresh packets, the checksum is not appended as mandated by the crypto-refresh. +## [3.0.0-alpha.2] 2024-04-12 ### Added - API to serialize KeyRings to binary data: ```go @@ -15,15 +29,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ```go func NewKeyRingFromBinary(binKeys []byte) (*KeyRing, error) ``` +- API to a create/verify plaintext detached signatures on the encryption/decryption handle instead of just encrypted detached signatures. -## [2.8.0-alpha.0] 2024-02-28 +## [3.0.0-alpha.1] 2024-03-20 +### Added +- Allow to override algorithm in key generation +- Always create a verification result on signature verification -### Added -- Adds support for the OpenPGP crypto-refresh by updating the go-crypto dependency to `v1.1.0-alpha.1`. -- Adapts the session key logic to handle PKESK/SKESK v6 packets without an algorithm attached -- Updates the min go version to `1.17` as required by go-crypto `v1.1.0-alpha.1`. -- Update the cricl dependency to `1.3.7` matching go-crypto. +### Changed +- Update ProtonMail/go-crypto to 1.1.0-alpha.2 +## [3.0.0-alpha.0] 2024-01-18 +### Added +- New simplified API that is not backward compatible. +- Full support for the crypto refresh. +- Improved interoperability with other OpenPGP libraries. +- Streaming support for all operations. +- Introduces profiles for OpenPGP customization. +- More documentation and examples. + +### Changed +- Mobile specific code is moved to the `mobile` package. +- Mime specific code is moved to the `mime` package. +- Replaces the go-crypto v1 API with the v2 API. + +### Removed +- The `helper` package, use the crypto package with the new API instead. +- `subtle` and `models` package. +- Time management code for retrieving and setting timestamps. ## [2.7.5] 2023-31-01 @@ -50,7 +83,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Ensure that `(SessionKey).Decrypt` functions return an error if no integrity protection is present in the encrypted input. To protect SEIPDv1 encrypted messages, SED packets must not be allowed in decryption. ## [2.7.3] 2023-08-28 -## Added +### Added - Add `helper.QuickCheckDecrypt` function to the helper package. The function allows to check with high probability if a session key can decrypt a SEIPDv1 data packet given its 24-byte prefix. ## [2.7.2] 2023-07-17 diff --git a/README.md b/README.md index 04b71b1b..4ea10305 100644 --- a/README.md +++ b/README.md @@ -1,376 +1,442 @@ -# GopenPGP V2 -[![Build Status](https://travis-ci.org/ProtonMail/gopenpgp.svg?branch=master)](https://travis-ci.org/ProtonMail/gopenpgp) +# GopenPGP V3 -GopenPGP is a high-level OpenPGP library built on top of [a fork of the golang -crypto library](https://github.com/ProtonMail/crypto). +[![Go Report Card](https://goreportcard.com/badge/github.com/ProtonMail/gopenpgp/v3)](https://goreportcard.com/report/github.com/ProtonMail/gopenpgp/v3) +[![GoDoc](https://godoc.org/github.com/ProtonMail/gopenpgp/v3?status.svg)](https://godoc.org/github.com/ProtonMail/gopenpgp/v3) + +GopenPGP V3 is a high-level OpenPGP library built on top of [a fork of the golang +crypto library](https://github.com/ProtonMail/go-crypto). **Table of Contents** -- [Download/Install](#downloadinstall) -- [Documentation](#documentation) -- [Using with Go Mobile](#using-with-go-mobile) -- [Full documentation](#full-documentation) -- [Examples](#examples) - - [Set up](#set-up) - - [Encrypt / Decrypt with password](#encrypt--decrypt-with-password) +- [GopenPGP V3](#gopenpgp-v3) + - [GopenPGP V2 support](#gopenpgp-v2-support) + - [Download/Install](#downloadinstall) + - [Documentation](#documentation) + - [Examples](#examples) + - [Encrypt / Decrypt with a password](#encrypt--decrypt-with-a-password) - [Encrypt / Decrypt with PGP keys](#encrypt--decrypt-with-pgp-keys) - [Generate key](#generate-key) - - [Detached signatures for plain text messages](#detached-signatures-for-plain-text-messages) - - [Detached signatures for binary data](#detached-signatures-for-binary-data) + - [Detached and inline signatures](#detached-and-inline-signatures) - [Cleartext signed messages](#cleartext-signed-messages) + - [Encrypt with different outputs](#encrypt-with-different-outputs) + - [Using with Go Mobile](#using-with-go-mobile) +## GopenPGP V2 support + +While GopenPGP V3 introduces a new API with significant enhancements, it is not backward compatible with GopenPGP V2. +Although we recommend upgrading to V3 for the latest features and improvements, we continue to support GopenPGP V2. +Our support includes ongoing bug fixes and minor feature updates to ensure stability and functionality for existing users. + +GopenPGP V2 can be accessed/modified via the [v2 branch of this repository](https://github.com/ProtonMail/gopenpgp/tree/v2). + ## Download/Install -### Vendored install -To use this library using [Go Modules](https://github.com/golang/go/wiki/Modules) just edit your -`go.mod` configuration to contain: -```gomod -require ( - ... - github.com/ProtonMail/gopenpgp/v2 v2.0.1 -) -``` -It can then be installed by running: -```sh -go mod vendor +To use GopenPGP with [Go Modules](https://github.com/golang/go/wiki/Modules) just run +``` +go get github.com/ProtonMail/gopenpgp/v3 ``` -Finally your software can include it in your software as follows: +in your project folder. + +Then, your code can include it as follows: ```go package main import ( "fmt" - "github.com/ProtonMail/gopenpgp/v2/crypto" + "github.com/ProtonMail/gopenpgp/v3/crypto" ) func main() { - fmt.Println(crypto.GetUnixTime()) + pgp := crypto.PGP() } ``` -### Git-Clone install -To install for development mode, cloning the repository, it can be done in the following way: -```bash -cd $GOPATH -mkdir -p src/github.com/ProtonMail/ -cd $GOPATH/src/github.com/ProtonMail/ -git clone git@github.com:ProtonMail/gopenpgp.git -cd gopenpgp -ln -s . v2 -go mod -``` - ## Documentation -A full overview of the API can be found here: -https://godoc.org/gopkg.in/ProtonMail/gopenpgp.v2/crypto -In this document examples are provided and the proper use of (almost) all functions is tested. - -## Using with Go Mobile -This library can be compiled with [Gomobile](https://github.com/golang/go/wiki/Mobile) too. -First ensure you have a working installation of gomobile: -```bash -gomobile version -``` -In case this fails, install it with: -```bash -go get -u golang.org/x/mobile/cmd/gomobile -``` -Then ensure your path env var has gomobile's binary, and it is properly init-ed: -```bash -export PATH="$PATH:$GOPATH/bin" -gomobile init -``` -Then you must ensure that the Android or iOS frameworks are installed and the respective env vars set. - -Finally, build the application -```bash -sh build.sh -``` -This script will build for both android and iOS at the same time, -to filter one out you can comment out the line in the corresponding section. +A full overview of the API can be found here: https://pkg.go.dev/github.com/ProtonMail/gopenpgp/v3. ## Examples -### Encrypt / Decrypt with password +A file of runnable examples can be found in [crypto_example_test.go](crypto/crypto_example_test.go). + +### Encrypt / Decrypt with a password ```go -import "github.com/ProtonMail/gopenpgp/v2/helper" +import "github.com/ProtonMail/gopenpgp/v3/crypto" -const password = []byte("hunter2") +password := []byte("hunter2") -// Encrypt data with password -armor, err := helper.EncryptMessageWithPassword(password, "my message") +pgp := crypto.PGP() +// Encrypt data with a password +encHandle, err := pgp.Encryption().Password(password).New() +pgpMessage, err := encHandle.Encrypt([]byte("my message")) +armored, err := pgpMessage.ArmorBytes() -// Decrypt data with password -message, err := helper.DecryptMessageWithPassword(password, armor) +// Decrypt data with a password +decHandle, err := pgp.Decryption().Password(password).New() +decrypted, err := decHandle.Decrypt(armored, crypto.Armor) +myMessage := decrypted.Bytes() ``` -To encrypt binary data or use more advanced modes: +To encrypt with the [latest proposed standard (RFC9580-to-be)](https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html): ```go -import "github.com/ProtonMail/gopenpgp/v2/constants" - -const password = []byte("hunter2") - -var message = crypto.NewPlainMessage(data) -// Or -message = crypto.NewPlainMessageFromString(string) +import "github.com/ProtonMail/gopenpgp/v3/profile" +// Use the profile that conforms with the crypto refresh (RFC9580-to-be). +pgp := crypto.PGPWithProfile(profile.RFC9580()) +// The default crypto refresh profile uses Argon2 for deriving +// session keys and uses an AEAD for encryption (AES-256, OCB mode). // Encrypt data with password -encrypted, err := EncryptMessageWithPassword(message, password) -// Encrypted message in encrypted.GetBinary() or encrypted.GetArmored() - +... // Decrypt data with password -decrypted, err := DecryptMessageWithPassword(encrypted, password) +... +``` + +Use a custom or preset profile: +```go +import "github.com/ProtonMail/gopenpgp/v3/profile" -//Original message in decrypted.GetBinary() +// RFC4880 profile +pgp4880 := crypto.PGPWithProfile(profile.RFC4880()) +// RFC9580 crypto refresh profile +pgpCryptoRefresh := crypto.PGPWithProfile(profile.RFC9580()) ``` ### Encrypt / Decrypt with PGP keys ```go -import "github.com/ProtonMail/gopenpgp/v2/helper" - -// put keys in backtick (``) to avoid errors caused by spaces or tabs +// Put keys in backtick (``) to avoid errors caused by spaces or tabs const pubkey = `-----BEGIN PGP PUBLIC KEY BLOCK----- ... -----END PGP PUBLIC KEY BLOCK-----` const privkey = `-----BEGIN PGP PRIVATE KEY BLOCK----- ... ------END PGP PRIVATE KEY BLOCK-----` // encrypted private key +-----END PGP PRIVATE KEY BLOCK-----` // Encrypted private key const passphrase = []byte(`the passphrase of the private key`) // Passphrase of the privKey +publicKey, err := crypto.NewKeyFromArmored(pubkey) +privateKey, err := crypto.NewPrivateKeyFromArmored(privkey, passphrase) -// encrypt plain text message using public key -armor, err := helper.EncryptMessageArmored(pubkey, "plain text") - -// decrypt armored encrypted message using the private key and obtain plain text -decrypted, err := helper.DecryptMessageArmored(privkey, passphrase, armor) +pgp := crypto.PGP() +// Encrypt plaintext message using a public key +encHandle, err := pgp.Encryption().Recipient(publicKey).New() +pgpMessage, err := encHandle.Encrypt([]byte("my message")) +armored, err := pgpMessage.ArmorBytes() -// encrypt binary message using public key -armor, err := helper.EncryptBinaryMessageArmored(pubkey, []byte("plain text")) +// Decrypt armored encrypted message using the private key and obtain the plaintext +decHandle, err := pgp.Decryption().DecryptionKey(privateKey).New() +decrypted, err := decHandle.Decrypt(armored, crypto.Armor) +myMessage := decrypted.Bytes() -// decrypt armored encrypted message using the private key expecting binary data -decrypted, err := helper.DecryptBinaryMessageArmored(privkey, passphrase, armor) +decHandle.ClearPrivateParams() ``` With signatures: ```go -// Keys initialization as before (omitted) - -// encrypt message using public key, sign with the private key -armor, err := helper.EncryptSignMessageArmored(pubkey, privkey, passphrase, "plain text") +pgp := crypto.PGP() +aliceKeyPriv, err := pgp.KeyGeneration(). + AddUserId("alice", "alice@alice.com"). + New(). + GenerateKey() +aliceKeyPub, err := aliceKeyPriv.ToPublic() + +bobKeyPriv, err := pgp.KeyGeneration(). + AddUserId("bob", "bob@bob.com"). + New(). + GenerateKey() +bobKeyPub, err := bobKeyPriv.ToPublic() + +// Encrypt and sign plaintext message from alice to bob +encHandle, err := pgp.Encryption(). + Recipient(bobKeyPub). + SigningKey(aliceKeyPriv). + New() +pgpMessage, err := encHandle.Encrypt([]byte("my message")) +armored, err := pgpMessage.ArmorBytes() + +// Decrypt armored encrypted message using the private key and obtain plain text +decHandle, err := pgp.Decryption(). + DecryptionKey(bobKeyPriv). + VerificationKey(aliceKeyPub). + New() +decrypted, err := decHandle.Decrypt(armored, crypto.Armor) +if sigErr := decrypted.SignatureError(); sigErr != nil { + // Signature verification failed with sigErr +} +myMessage := decrypted.Bytes() -// decrypt armored encrypted message using the private key, verify with the public key -// err != nil if verification fails -decrypted, err := helper.DecryptVerifyMessageArmored(pubkey, privkey, passphrase, armor) +encHandle.ClearPrivateParams() +decHandle.ClearPrivateParams() ``` - -For more advanced modes the full API (i.e. without helpers) can be used: +Encrypt towards multiple recipients: ```go -// Keys initialization as before (omitted) -var binMessage = crypto.NewPlainMessage(data) - -publicKeyObj, err := crypto.NewKeyFromArmored(publicKey) -publicKeyRing, err := crypto.NewKeyRing(publicKeyObj) - -pgpMessage, err := publicKeyRing.Encrypt(binMessage, privateKeyRing) - -// Armored message in pgpMessage.GetArmored() -// pgpMessage can be obtained from NewPGPMessageFromArmored(ciphertext) - -//pgpMessage can be obtained from a byte array -var pgpMessage = crypto.NewPGPMessage([]byte) - -privateKeyObj, err := crypto.NewKeyFromArmored(privateKey) -unlockedKeyObj = privateKeyObj.Unlock(passphrase) -privateKeyRing, err := crypto.NewKeyRing(unlockedKeyObj) - -message, err := privateKeyRing.Decrypt(pgpMessage, publicKeyRing, crypto.GetUnixTime()) - -privateKeyRing.ClearPrivateParams() +recipients, err := crypto.NewKeyRing(bobKeyPub) +err = recipients.AddKey(carolKeyPub) +// encrypt plain text message using a public key +encHandle, err := pgp.Encryption(). + Recipients(recipients). + SigningKey(aliceKeyPriv). + New() +pgpMessage, err := encHandle.Encrypt([]byte("my message")) +armored, err := pgpMessage.ArmorBytes() + +encHandle.ClearPrivateParams() +``` -// Original data in message.GetString() -// `err` can be a SignatureVerificationError +Encrypt towards an (anonymous) recipient: +```go +//... +// The key fingerprint of bob's key is visible in the key packet and +// is included in the signature's intended recipient list. +// The key fingerprint of carols's key is not visible in the key packet ("anonymous" key packet), and +// is not included in the signature's intended recipient list. +encHandle, _ := pgp.Encryption(). + Recipient(bobKeyPub). + HiddenRecipient(carolKeyPub). + SigningKey(aliceKeyPriv). + New() +pgpMessage, _ := encHandle.Encrypt([]byte("my message")) + +// Decrypt checks if bobs key fingerprint is in the intended recipient list +// of alice's signature in the message. +decHandleBob, _ := pgp.Decryption(). + DecryptionKey(bobKeyPriv). + VerificationKey(aliceKeyPub). + New() +decryptedBob, _ := decHandleBob.Decrypt(pgpMessage.Bytes(), crypto.Bytes) +fmt.Println(string(decryptedBob.Bytes())) + +// Disable intended recipient check, there is no info about carols key in the message. +// The decryption function tries all supplied keys for decrypting the "anonymous" key packet. +// If the check is not disabled, the decryption result would contain a signature error. +decHandleCarol, _ := pgp.Decryption(). + DecryptionKey(carolKeyPriv). + VerificationKey(aliceKeyPub). + DisableIntendedRecipients(). + New() +decryptedCarol, _ := decHandleCarol.Decrypt(pgpMessage.Bytes(), crypto.Bytes) ``` +Encrypt and decrypt large messages with the streaming API: +```go +pgp := crypto.PGP() +// ... See key generation above + +// Encrypt plain text stream and write the output to a file +encHandle, err := pgp.Encryption(). + Recipient(bobKeyPub). + SigningKey(aliceKeyPriv). + New() +messageReader, err := os.Open("msg.txt") +ciphertextWriter, err := os.Create("out.pgp") + +ptWriter, err := encHandle.EncryptingWriter(ciphertextWriter, crypto.Armor) +_, err = io.Copy(ptWriter, messageReader) +err = ptWriter.Close() +err = messageReader.Close() +err = ciphertextWriter.Close() + +ctFileRead, err := os.Open("out.pgp") +defer ctFileRead.Close() +// Decrypt stream and read the result to memory +decHandle, err := pgp.Decryption(). + DecryptionKey(bobKeyPriv). + VerificationKey(aliceKeyPub). + New() +ptReader, err := decHandle.DecryptingReader(ctFileRead, crypto.Armor) +decResult, err := ptReader.ReadAllAndVerifySignature() +if sigErr := decResult.SignatureError(); sigErr != nil { + // Handle sigErr +} +// Access decrypted message with decResult.Bytes() +``` ### Generate key -Keys are generated with the `GenerateKey` function, that returns the armored key as a string and a potential error. -The library supports RSA with different key lengths or Curve25519 keys. - +Keys are generated with the `GenerateKey` function on the pgp handle. ```go +import "github.com/ProtonMail/gopenpgp/v3/constants" + const ( name = "Max Mustermann" email = "max.mustermann@example.com" passphrase = []byte("LongSecret") - rsaBits = 2048 ) -// RSA, string -rsaKey, err := helper.GenerateKey(name, email, passphrase, "rsa", rsaBits) - -// Curve25519, string -ecKey, err := helper.GenerateKey(name, email, passphrase, "x25519", 0) +pgpDefault := crypto.PGPWithProfile(profile.Default()) +pgp4880 := crypto.PGPWithProfile(profile.RFC4880()) +pgpCryptoRefresh := crypto.PGPWithProfile(profile.RFC9580()) + +// Note that RSA keys should not be generated anymore according to +// RFC9580 (crypto refresh). + +keyGenHandle := pgp4880.KeyGeneration().AddUserId(name, email).New() +// Generates rsa keys with 3072 bits +rsaKey, err := keyGenHandle.GenerateKey() +// Generates rsa keys with 4092 bits +rsaKeyHigh, err := keyGenHandle.GenerateKeyWithSecurity(constants.HighSecurity) + +keyGenHandle = pgpDefault.KeyGeneration().AddUserId(name, email).New() +// Generates curve25519 v4 keys. +ecKey, err := keyGenHandle.GenerateKey() +// Generates curve448 v4 keys. +ecKeyHigh, err := keyGenHandle.GenerateKeyWithSecurity(constants.HighSecurity) + +keyGenHandle = pgpCryptoRefresh.KeyGeneration().AddUserId(name, email).New() +// Generates curve25519 v6 keys with RFC9580 (crypto refresh). +ecKey, err = keyGenHandle.GenerateKey() +// Generates curve448 v6 keys with RFC9580 (crypto refresh). +ecKeyHigh, err = keyGenHandle.GenerateKeyWithSecurity(constants.HighSecurity) +``` -// RSA, Key struct -rsaKey, err := crypto.GenerateKey(name, email, "rsa", rsaBits) +Encrypt (lock) and decrypt (unlock) a secret key: +```go +password := []byte("password") -// Curve25519, Key struct -ecKey, err := crypto.GenerateKey(name, email, "x25519", 0) +pgp := crypto.PGP() +// Encrypt key with password +lockedKey, err := pgp.LockKey(aliceKeyPriv, password) +// Decrypt key with password +unlockedKey, err := lockedKey.Unlock(password) ``` -### Detached signatures for plain text messages +### Detached and inline signatures -To sign plain text data either an unlocked private keyring or a passphrase must be provided. -The output is an armored signature. +Sign a plaintext with a private key and verify it with its public key using detached signatures: ```go -const privkey = `-----BEGIN PGP PRIVATE KEY BLOCK----- -... ------END PGP PRIVATE KEY BLOCK-----` // Encrypted private key -const passphrase = []byte("LongSecret") // Private key passphrase +pgp := crypto.PGP() +// ... See generating keys -var message = crypto.NewPlaintextMessage("Verified message") +signingMessage := []byte("message to sign") -privateKeyObj, err := crypto.NewKeyFromArmored(privkey) -unlockedKeyObj = privateKeyObj.Unlock(passphrase) -signingKeyRing, err := crypto.NewKeyRing(unlockedKeyObj) +signer, err := pgp.Sign().SigningKey(aliceKeyPriv).Detached().New() +signature, err := signer.Sign(signingMessage, crypto.Armor) -pgpSignature, err := signingKeyRing.SignDetached(message, trimNewlines) +verifier, err := pgp.Verify().VerificationKey(aliceKeyPub).New() +verifyResult, err := verifier.VerifyDetached(signingMessage, signature, crypto.Armor) +if sigErr := verifyResult.SignatureError(); sigErr != nil { + // Handle sigErr +} -// The armored signature is in pgpSignature.GetArmored() -// The signed text is in message.GetString() +signer.ClearPrivateParams() ``` -To verify a signature either private or public keyring can be provided. -```go -const pubkey = `-----BEGIN PGP PUBLIC KEY BLOCK----- -... ------END PGP PUBLIC KEY BLOCK-----` - -const signature = `-----BEGIN PGP SIGNATURE----- -... ------END PGP SIGNATURE-----` +Sign a plaintext with a private key and verify it with its public key using inline signatures: -message := crypto.NewPlaintextMessage("Verified message") -pgpSignature, err := crypto.NewPGPSignatureFromArmored(signature) +```go +pgp := crypto.PGP() +// ... See generating keys -publicKeyObj, err := crypto.NewKeyFromArmored(pubkey) -signingKeyRing, err := crypto.NewKeyRing(publicKeyObj) +signingMessage := []byte("message to sign") -err := signingKeyRing.VerifyDetached(message, pgpSignature, crypto.GetUnixTime()) +signer, err := pgp.Sign().SigningKey(aliceKeyPriv).New() +signatureMessage, err := signer.Sign(signingMessage, crypto.Armor) -if err == nil { - // verification success +verifier, err := pgp.Verify().VerificationKey(aliceKeyPub).New() +verifyResult, err := verifier.VerifyInline(signatureMessage, crypto.Armor) +if sigErr := verifyResult.SignatureError(); sigErr != nil { + // Handle sigErr } +// Access signed data with verifyResult.Bytes() +signer.ClearPrivateParams() ``` -### Detached signatures for binary data - -```go -const privkey = `-----BEGIN PGP PRIVATE KEY BLOCK----- -... ------END PGP PRIVATE KEY BLOCK-----` // encrypted private key -const passphrase = "LongSecret" - -var message = crypto.NewPlainMessage(data) - -privateKeyObj, err := crypto.NewKeyFromArmored(privkey) -unlockedKeyObj := privateKeyObj.Unlock(passphrase) -signingKeyRing, err := crypto.NewKeyRing(unlockedKeyObj) - -pgpSignature, err := signingKeyRing.SignDetached(message) - -// The armored signature is in pgpSignature.GetArmored() -// The signed text is in message.GetBinary() -``` -To verify a signature either private or public keyring can be provided. +### Cleartext signed messages ```go -const pubkey = `-----BEGIN PGP PUBLIC KEY BLOCK----- -... ------END PGP PUBLIC KEY BLOCK-----` - -const signature = `-----BEGIN PGP SIGNATURE----- -... ------END PGP SIGNATURE-----` - -message := crypto.NewPlainMessage("Verified message") -pgpSignature, err := crypto.NewPGPSignatureFromArmored(signature) - -publicKeyObj, err := crypto.NewKeyFromArmored(pubkey) -signingKeyRing, err := crypto.NewKeyRing(publicKeyObj) - -err := signingKeyRing.VerifyDetached(message, pgpSignature, crypto.GetUnixTime()) - -if err == nil { - // verification success +pgp := crypto.PGP() +// ... See generating keys + +signingMessage := []byte("message to sign") + +signer, err := pgp.Sign().SigningKey(aliceKeyPriv).New() +cleartextArmored, err := signer.SignCleartext(signingMessage) +// CleartextArmored has the form: +// -----BEGIN PGP SIGNED MESSAGE----- +// ... +// -----BEGIN PGP SIGNATURE----- +// ... +// -----END PGP SIGNATURE----- + +verifier, err := pgp.Verify().VerificationKey(aliceKeyPub).New() +verifyResult, err := verifier.VerifyCleartext(cleartextArmored) +if sigErr := verifyResult.SignatureError(); sigErr != nil { + // Handle sigErr } -``` -### Cleartext signed messages -```go -// Keys initialization as before (omitted) -armored, err := helper.SignCleartextMessageArmored(privateKey, passphrase, plaintext) +signer.ClearPrivateParams() ``` -To verify the message it has to be provided unseparated to the library. -If verification fails an error will be returned. -```go -// Keys initialization as before (omitted) -verifiedPlainText, err := helper.VerifyCleartextMessageArmored(publicKey, armored, crypto.GetUnixTime()) -``` +### Encrypt with different outputs -### Encrypting and decrypting session Keys -A session key can be generated, encrypted to a Asymmetric/Symmetric key packet and obtained from it +Split encrypted message into key packets and data packets ```go -// Keys initialization as before (omitted) - -sessionKey, err := crypto.GenerateSessionKey() - -keyPacket, err := publicKeyRing.EncryptSessionKey(sessionKey) // Will encrypt to all the keys in the keyring -keyPacketSymm, err := crypto.EncryptSessionKeyWithPassword(sessionKey, password) +// Non-streaming +pgpMessage, err := encHandle.Encrypt(...) +keyPackets := pgpMessage.BinaryKeyPacket() +dataPackets := pgpMessage.BinaryDataPacket() + +// Streaming +var keyPackets bytes.Buffer +var dataPackets bytes.Buffer +splitWriter := crypto.NewPGPSplitWriterKeyAndData(&keyPackets, &dataPackets) +ptWriter, _ := encHandle.EncryptingWriter(splitWriter, crypto.Bytes) +// ... +// Key packets are written to keyPackets while data packets are written to dataPackets ``` -`KeyPacket` is a `[]byte` containing the session key encrypted with the public key or password. +Produce encrypted detached signatures instead of embedded signatures: ```go -decodedKeyPacket, err := privateKeyRing.DecryptSessionKey(keyPacket) // Will decode with the first valid key found -decodedSymmKeyPacket, err := crypto.DecryptSessionKeyWithPassword(keyPacketSymm, password) +// Non-streaming +encHandle, err := pgp.Encryption(). + Recipient(bobKeyPub). + SigningKey(aliceKeyPriv). + DetachedSignature(). + New() // Enable the detached signature option +pgpMessage, err := encHandle.Encrypt(...) +pgpMessageEncSig, err := pgpMessage.EncryptedDetachedSignature() +// pgpMessage.Bytes() encrypted message without an embedded signature +// pgpMessageEncSig.Bytes() encrypted signature message +// pgpMessage: key packets|enc data packets +// pgpMessageEncSig: key packets|enc signature packet + + +// Streaming +// ... +var encSigDataPackets bytes.Buffer +splitWriter := crypto.NewPGPSplitWriter(&keyPackets, &dataPackets, &encSigDataPackets) +ptWriter, err := encHandle.EncryptingWriter(splitWriter, crypto.Bytes) +// ... +// Key packets are written to keyPackets, data packets are written to dataPackets ,and +// Data packets of the encrypted signature to encSigDataPackets ``` -`decodedKeyPacket` and `decodedSymmKeyPacket` are objects of type `*SymmetricKey` that can -be used to decrypt the corresponding symmetrically encrypted data packets: - -```go -var message = crypto.NewPlainMessage(data) - -// Encrypt data with session key -dataPacket, err := sessionKey.Encrypt(message) - -// Decrypt data with session key -decrypted, err := sessionKey.Decrypt(password, dataPacket) -//Original message in decrypted.GetBinary() +## Using with Go Mobile +This library can be compiled with [Gomobile](https://github.com/golang/go/wiki/Mobile) too. +First ensure you have a working installation of gomobile: +```bash +gomobile version ``` - -Note that it is not possible to process signatures when using data packets directly. -Joining the data packet and a key packet gives us a valid PGP message: - -```go -pgpSplitMessage := NewPGPSplitMessage(keyPacket, dataPacket) -pgpMessage := pgpSplitMessage.GetPGPMessage() - -// And vice-versa -newPGPSplitMessage, err := pgpMessage.SeparateKeyAndData() -// Key Packet is in newPGPSplitMessage.GetBinaryKeyPacket() -// Data Packet is in newPGPSplitMessage.GetBinaryDataPacket() +In case this fails, install it with: +```bash +go get -u golang.org/x/mobile/cmd/gomobile +``` +Then ensure your path env var has gomobile's binary, and it is properly init-ed: +```bash +export PATH="$PATH:$GOPATH/bin" +gomobile init ``` +Then you must ensure that the Android or iOS frameworks are installed and the respective env vars set. -### Checking keys -Keys are now checked on import and the explicit check via `Key#Check()` is deprecated and no longer necessary. \ No newline at end of file +Finally, build the application +```bash +sh build.sh +``` +This script will build for both android and iOS at the same time, +to filter one out you can comment out the line in the corresponding section. diff --git a/armor/armor.go b/armor/armor.go index 2ad88350..9a9a7c18 100644 --- a/armor/armor.go +++ b/armor/armor.go @@ -5,11 +5,10 @@ package armor import ( "bytes" "io" - "io/ioutil" "github.com/ProtonMail/go-crypto/openpgp/armor" - "github.com/ProtonMail/gopenpgp/v2/constants" - "github.com/ProtonMail/gopenpgp/v2/internal" + "github.com/ProtonMail/gopenpgp/v3/constants" + "github.com/ProtonMail/gopenpgp/v3/internal" "github.com/pkg/errors" ) @@ -18,20 +17,87 @@ func ArmorKey(input []byte) (string, error) { return ArmorWithType(input, constants.PublicKeyHeader) } -// ArmorWithTypeBuffered returns a io.WriteCloser which, when written to, writes +// ArmorWriterWithType returns a io.WriteCloser which, when written to, writes // armored data to w with the given armorType. -func ArmorWithTypeBuffered(w io.Writer, armorType string) (io.WriteCloser, error) { - return armor.Encode(w, armorType, nil) +func ArmorWriterWithType(w io.Writer, armorType string) (io.WriteCloser, error) { + return armor.EncodeWithChecksumOption(w, armorType, internal.ArmorHeaders, constants.ArmorChecksumEnabled) +} + +// ArmorWriterWithTypeChecksum returns a io.WriteCloser which, when written to, writes +// armored data to w with the given armorType. +// The checksum determines if an armor checksum is written at the end. +func ArmorWriterWithTypeChecksum(w io.Writer, armorType string, checksum bool) (io.WriteCloser, error) { + return armor.EncodeWithChecksumOption(w, armorType, internal.ArmorHeaders, checksum) +} + +// ArmorWriterWithTypeAndCustomHeaders returns a io.WriteCloser, +// which armors input with the given armorType and headers. +func ArmorWriterWithTypeAndCustomHeaders(w io.Writer, armorType, version, comment string) (io.WriteCloser, error) { + headers := make(map[string]string) + if version != "" { + headers["Version"] = version + } + if comment != "" { + headers["Comment"] = comment + } + return armor.EncodeWithChecksumOption(w, armorType, headers, constants.ArmorChecksumEnabled) } // ArmorWithType armors input with the given armorType. func ArmorWithType(input []byte, armorType string) (string, error) { - return armorWithTypeAndHeaders(input, armorType, internal.ArmorHeaders) + return ArmorWithTypeChecksum(input, armorType, constants.ArmorChecksumEnabled) +} + +// ArmorWithTypeChecksum armors input with the given armorType. +// The checksum option determines if an armor checksum is written at the end. +func ArmorWithTypeChecksum(input []byte, armorType string, checksum bool) (string, error) { + buffer, err := armorWithTypeAndHeaders(input, armorType, internal.ArmorHeaders, checksum) + if err != nil { + return "", err + } + return buffer.String(), err +} + +// ArmorWithTypeBytes armors input with the given armorType. +func ArmorWithTypeBytes(input []byte, armorType string) ([]byte, error) { + return ArmorWithTypeBytesChecksum(input, armorType, constants.ArmorChecksumEnabled) +} + +// ArmorWithTypeBytesChecksum armors input with the given armorType and checksum option. +func ArmorWithTypeBytesChecksum(input []byte, armorType string, checksum bool) ([]byte, error) { + buffer, err := armorWithTypeAndHeaders(input, armorType, internal.ArmorHeaders, checksum) + if err != nil { + return nil, err + } + return buffer.Bytes(), err } // ArmorWithTypeAndCustomHeaders armors input with the given armorType and // headers. func ArmorWithTypeAndCustomHeaders(input []byte, armorType, version, comment string) (string, error) { + return ArmorWithTypeAndCustomHeadersChecksum(input, armorType, version, comment, constants.ArmorChecksumEnabled) +} + +// ArmorWithTypeAndCustomHeadersChecksum armors input with the given armorType and +// headers and checksum option. +func ArmorWithTypeAndCustomHeadersChecksum(input []byte, armorType, version, comment string, checksum bool) (string, error) { + headers := make(map[string]string) + if version != "" { + headers["Version"] = version + } + if comment != "" { + headers["Comment"] = comment + } + buffer, err := armorWithTypeAndHeaders(input, armorType, headers, checksum) + if err != nil { + return "", err + } + return buffer.String(), err +} + +// ArmorWithTypeAndCustomHeadersBytes armors input with the given armorType and +// headers. +func ArmorWithTypeAndCustomHeadersBytes(input []byte, armorType, version, comment string) ([]byte, error) { headers := make(map[string]string) if version != "" { headers["Version"] = version @@ -39,31 +105,96 @@ func ArmorWithTypeAndCustomHeaders(input []byte, armorType, version, comment str if comment != "" { headers["Comment"] = comment } - return armorWithTypeAndHeaders(input, armorType, headers) + buffer, err := armorWithTypeAndHeaders(input, armorType, headers, constants.ArmorChecksumEnabled) + if err != nil { + return nil, err + } + return buffer.Bytes(), err +} + +// ArmorReader returns a io.Reader which, when read, reads +// unarmored data from in. +func ArmorReader(in io.Reader) (io.Reader, error) { + block, err := armor.Decode(in) + if err != nil { + return nil, err + } + return block.Body, nil } // Unarmor unarmors an armored input into a byte array. func Unarmor(input string) ([]byte, error) { b, err := internal.Unarmor(input) if err != nil { - return nil, errors.Wrap(err, "gopengp: unable to unarmor") + return nil, errors.Wrap(err, "armor: unable to unarmor") + } + return io.ReadAll(b.Body) +} + +// UnarmorBytes unarmors an armored input into a byte array. +func UnarmorBytes(input []byte) ([]byte, error) { + b, err := internal.UnarmorBytes(input) + if err != nil { + return nil, errors.Wrap(err, "armor: unable to unarmor") + } + return io.ReadAll(b.Body) +} + +func ArmorPGPSignatureBinary(signature []byte) ([]byte, error) { + return ArmorWithTypeBytes(signature, constants.PGPSignatureHeader) +} + +func ArmorPGPSignature(signature []byte) (string, error) { + return ArmorWithType(signature, constants.PGPSignatureHeader) +} + +func ArmorPGPMessageBytes(signature []byte) ([]byte, error) { + return ArmorWithTypeBytes(signature, constants.PGPMessageHeader) +} + +func ArmorPGPMessage(signature []byte) (string, error) { + return ArmorWithType(signature, constants.PGPMessageHeader) +} + +func ArmorPGPMessageBytesChecksum(signature []byte, checksum bool) ([]byte, error) { + return ArmorWithTypeBytesChecksum(signature, constants.PGPMessageHeader, checksum) +} + +func ArmorPGPMessageChecksum(signature []byte, checksum bool) (string, error) { + return ArmorWithTypeChecksum(signature, constants.PGPMessageHeader, checksum) +} + +const armorPrefix = "-----BEGIN PGP" +const maxGarbageBytes = 128 + +// IsPGPArmored reads a prefix from the reader and checks +// if it is equal to a pgp armored message prefix. +// Returns an io.Reader that is reset to the state of the in reader, +// and a bool that indicates if there is a match. +// If reading from the reader fails, the returned bool is set to false. +func IsPGPArmored(in io.Reader) (io.Reader, bool) { + buffer := make([]byte, len(armorPrefix)+maxGarbageBytes) + n, _ := io.ReadFull(in, buffer) + outReader := io.MultiReader(bytes.NewReader(buffer[:n]), in) + if bytes.Contains(buffer[:n], []byte(armorPrefix)) { + return outReader, true } - return ioutil.ReadAll(b.Body) + return outReader, false } -func armorWithTypeAndHeaders(input []byte, armorType string, headers map[string]string) (string, error) { +func armorWithTypeAndHeaders(input []byte, armorType string, headers map[string]string, writeChecksum bool) (*bytes.Buffer, error) { var b bytes.Buffer - w, err := armor.Encode(&b, armorType, headers) + w, err := armor.EncodeWithChecksumOption(&b, armorType, headers, writeChecksum) if err != nil { - return "", errors.Wrap(err, "gopengp: unable to encode armoring") + return nil, errors.Wrap(err, "armor: unable to encode armoring") } if _, err = w.Write(input); err != nil { - return "", errors.Wrap(err, "gopengp: unable to write armored to buffer") + return nil, errors.Wrap(err, "armor: unable to write armored to buffer") } if err := w.Close(); err != nil { - return "", errors.Wrap(err, "gopengp: unable to close armor buffer") + return nil, errors.Wrap(err, "armor: unable to close armor buffer") } - return b.String(), nil + return &b, nil } diff --git a/build.sh b/build.sh index 17aa6bc7..1475df83 100755 --- a/build.sh +++ b/build.sh @@ -96,12 +96,12 @@ BUILD_NAME="gopenpgp" # ==== Packages to included ===== PACKAGES="" ## crypto must be the first one, and the framework name better same with the first package name -import github.com/ProtonMail/gopenpgp/v2/crypto -import github.com/ProtonMail/gopenpgp/v2/armor -import github.com/ProtonMail/gopenpgp/v2/constants -import github.com/ProtonMail/gopenpgp/v2/models -import github.com/ProtonMail/gopenpgp/v2/subtle -import github.com/ProtonMail/gopenpgp/v2/helper +import github.com/ProtonMail/gopenpgp/v3/crypto +import github.com/ProtonMail/gopenpgp/v3/armor +import github.com/ProtonMail/gopenpgp/v3/constants +import github.com/ProtonMail/gopenpgp/v3/mime +import github.com/ProtonMail/gopenpgp/v3/mobile +import github.com/ProtonMail/gopenpgp/v3/profile ######## ======== Main =========== diff --git a/constants/armor.go b/constants/armor.go index 8420af25..e72d8871 100644 --- a/constants/armor.go +++ b/constants/armor.go @@ -3,10 +3,18 @@ package constants // Constants for armored data. const ( - ArmorHeaderVersion = "GopenPGP 2.8.0" - ArmorHeaderComment = "https://gopenpgp.org" - PGPMessageHeader = "PGP MESSAGE" - PGPSignatureHeader = "PGP SIGNATURE" - PublicKeyHeader = "PGP PUBLIC KEY BLOCK" - PrivateKeyHeader = "PGP PRIVATE KEY BLOCK" + // ArmorChecksumEnabled defines the default behavior for adding an armor checksum + // to an armored message. + // + // If set to true, an armor checksum is added to the message. + // + // If set to false, no armor checksum is added. + ArmorChecksumEnabled = true + ArmorHeaderEnabled = false // can be enabled for debugging at compile time only + ArmorHeaderVersion = "GopenPGP " + Version + ArmorHeaderComment = "https://gopenpgp.org" + PGPMessageHeader = "PGP MESSAGE" + PGPSignatureHeader = "PGP SIGNATURE" + PublicKeyHeader = "PGP PUBLIC KEY BLOCK" + PrivateKeyHeader = "PGP PRIVATE KEY BLOCK" ) diff --git a/constants/cipher.go b/constants/cipher.go index ab8c72ee..3dc4d96c 100644 --- a/constants/cipher.go +++ b/constants/cipher.go @@ -18,5 +18,22 @@ const ( SIGNATURE_BAD_CONTEXT int = 4 ) -const DefaultCompression = 2 // ZLIB -const DefaultCompressionLevel = 6 // Corresponds to default -1 for ZLIB +// SecurityLevel constants. +// The type is int8 for compatibility with gomobile. +const ( + // StandardSecurity is the default security level. + StandardSecurity int8 = 0 + // HighSecurity is the high security level. + HighSecurity int8 = 1 +) + +// Wraps the packet.CipherFunction enum from go-crypto +// for go-mobile clients. +// int8 type for go-mobile support. +const ( + Cipher3DES int8 = 2 + CipherCAST5 int8 = 3 + CipherAES128 int8 = 7 + CipherAES192 int8 = 8 + CipherAES256 int8 = 9 +) diff --git a/constants/compression.go b/constants/compression.go new file mode 100644 index 00000000..662df583 --- /dev/null +++ b/constants/compression.go @@ -0,0 +1,12 @@ +package constants + +const ( + // Use no compression (default). + NoCompression int8 = 0 + // Use compression defined by the pgp profile. + DefaultCompression int8 = 1 + // Use ZIP compression. + ZIPCompression int8 = 2 + // Use ZLIB compression. + ZLIBCompression int8 = 3 +) diff --git a/constants/signature.go b/constants/signature.go new file mode 100644 index 00000000..f1090fc2 --- /dev/null +++ b/constants/signature.go @@ -0,0 +1,18 @@ +package constants + +// OpenPGP signature types. +// int8 type for go-mobile clients. +const ( + SigTypeBinary int8 = 0x00 + SigTypeText int8 = 0x01 + SigTypeGenericCert int8 = 0x10 + SigTypePersonaCert int8 = 0x11 + SigTypeCasualCert int8 = 0x12 + SigTypePositiveCert int8 = 0x13 + SigTypeSubkeyBinding int8 = 0x18 + SigTypePrimaryKeyBinding int8 = 0x19 + SigTypeDirectSignature int8 = 0x1F + SigTypeKeyRevocation int8 = 0x20 + SigTypeSubkeyRevocation int8 = 0x28 + SigTypeCertificationRevocation int8 = 0x30 +) diff --git a/constants/version.go b/constants/version.go index 7ab3b2cb..1164f6db 100644 --- a/constants/version.go +++ b/constants/version.go @@ -1,3 +1,3 @@ package constants -const Version = "2.8.0" +const Version = "3.0.0-alpha.3" diff --git a/crypto/attachment.go b/crypto/attachment.go deleted file mode 100644 index b078ce1c..00000000 --- a/crypto/attachment.go +++ /dev/null @@ -1,185 +0,0 @@ -package crypto - -import ( - "bytes" - "io" - "io/ioutil" - "runtime" - "sync" - "time" - - "github.com/ProtonMail/go-crypto/openpgp" - "github.com/ProtonMail/go-crypto/openpgp/packet" - "github.com/pkg/errors" -) - -// AttachmentProcessor keeps track of the progress of encrypting an attachment -// (optimized for encrypting large files). -type AttachmentProcessor struct { - w *io.WriteCloser - pipe *io.PipeWriter - done sync.WaitGroup - split *PGPSplitMessage - garbageCollector int - err error -} - -// Process writes attachment data to be encrypted. -func (ap *AttachmentProcessor) Process(plainData []byte) { - if _, err := (*ap.w).Write(plainData); err != nil { - panic(err) - } - if ap.garbageCollector > 0 { - defer runtime.GC() - } -} - -// Finish closes the attachment and returns the encrypted data. -func (ap *AttachmentProcessor) Finish() (*PGPSplitMessage, error) { - if ap.err != nil { - return nil, ap.err - } - - if err := (*ap.w).Close(); err != nil { - return nil, errors.Wrap(err, "gopengpp: unable to close writer") - } - - if ap.garbageCollector > 0 { - ap.w = nil - runtime.GC() - } - - if err := (*ap.pipe).Close(); err != nil { - return nil, errors.Wrap(err, "gopengpp: unable to close pipe") - } - - ap.done.Wait() - if ap.err != nil { - return nil, ap.err - } - splitMsg := ap.split - - if ap.garbageCollector > 0 { - ap.pipe = nil - ap.split = nil - defer runtime.GC() - } - return splitMsg, nil -} - -// newAttachmentProcessor creates an AttachmentProcessor which can be used to encrypt -// a file. It takes an estimatedSize and fileName as hints about the file. -func (keyRing *KeyRing) newAttachmentProcessor( - estimatedSize int, filename string, isBinary bool, modTime uint32, garbageCollector int, //nolint:unparam -) (*AttachmentProcessor, error) { - attachmentProc := &AttachmentProcessor{} - // You could also add these one at a time if needed. - attachmentProc.done.Add(1) - attachmentProc.garbageCollector = garbageCollector - - hints := &openpgp.FileHints{ - FileName: filename, - IsBinary: isBinary, - ModTime: time.Unix(int64(modTime), 0), - } - - config := &packet.Config{ - DefaultCipher: packet.CipherAES256, - Time: getTimeGenerator(), - } - - reader, writer := io.Pipe() - - go func() { - defer attachmentProc.done.Done() - ciphertext, _ := ioutil.ReadAll(reader) - message := &PGPMessage{ - Data: ciphertext, - } - split, splitError := message.SplitMessage() - if attachmentProc.err == nil { - attachmentProc.err = splitError - } - attachmentProc.split = split - }() - - var ew io.WriteCloser - var encryptErr error - ew, encryptErr = openpgp.Encrypt(writer, keyRing.entities, nil, hints, config) - if encryptErr != nil { - return nil, errors.Wrap(encryptErr, "gopengpp: unable to encrypt attachment") - } - attachmentProc.w = &ew - attachmentProc.pipe = writer - - return attachmentProc, nil -} - -// EncryptAttachment encrypts a file given a PlainMessage and a filename. -// If given a filename it will override the information in the PlainMessage object. -// Returns a PGPSplitMessage containing a session key packet and symmetrically encrypted data. -// Specifically designed for attachments rather than text messages. -func (keyRing *KeyRing) EncryptAttachment(message *PlainMessage, filename string) (*PGPSplitMessage, error) { - if filename == "" { - filename = message.Filename - } - - ap, err := keyRing.newAttachmentProcessor( - len(message.GetBinary()), - filename, - message.IsBinary(), - message.Time, - -1, - ) - if err != nil { - return nil, err - } - ap.Process(message.GetBinary()) - split, err := ap.Finish() - if err != nil { - return nil, err - } - return split, nil -} - -// NewLowMemoryAttachmentProcessor creates an AttachmentProcessor which can be used -// to encrypt a file. It takes an estimatedSize and filename as hints about the -// file. It is optimized for low-memory environments and collects garbage every -// megabyte. -func (keyRing *KeyRing) NewLowMemoryAttachmentProcessor( - estimatedSize int, filename string, -) (*AttachmentProcessor, error) { - return keyRing.newAttachmentProcessor(estimatedSize, filename, true, uint32(GetUnixTime()), 1<<20) -} - -// DecryptAttachment takes a PGPSplitMessage, containing a session key packet and symmetrically encrypted data -// and returns a decrypted PlainMessage -// Specifically designed for attachments rather than text messages. -func (keyRing *KeyRing) DecryptAttachment(message *PGPSplitMessage) (*PlainMessage, error) { - privKeyEntries := keyRing.entities - - keyReader := bytes.NewReader(message.GetBinaryKeyPacket()) - dataReader := bytes.NewReader(message.GetBinaryDataPacket()) - - encryptedReader := io.MultiReader(keyReader, dataReader) - - config := &packet.Config{Time: getTimeGenerator()} - - md, err := openpgp.ReadMessage(encryptedReader, privKeyEntries, nil, config) - if err != nil { - return nil, errors.Wrap(err, "gopengpp: unable to read attachment") - } - - decrypted := md.UnverifiedBody - b, err := ioutil.ReadAll(decrypted) - if err != nil { - return nil, errors.Wrap(err, "gopengpp: unable to read attachment body") - } - - return &PlainMessage{ - Data: b, - TextType: !md.LiteralData.IsBinary, - Filename: md.LiteralData.FileName, - Time: md.LiteralData.Time, - }, nil -} diff --git a/crypto/attachment_manual.go b/crypto/attachment_manual.go deleted file mode 100644 index 39a2957a..00000000 --- a/crypto/attachment_manual.go +++ /dev/null @@ -1,188 +0,0 @@ -package crypto - -import ( - "io" - "io/ioutil" - "runtime" - "runtime/debug" - "sync" - "time" - - "github.com/ProtonMail/go-crypto/openpgp" - "github.com/ProtonMail/go-crypto/openpgp/packet" - "github.com/pkg/errors" -) - -// ManualAttachmentProcessor keeps track of the progress of encrypting an attachment -// (optimized for encrypting large files). -// With this processor, the caller has to first allocate -// a buffer large enough to hold the whole data packet. -type ManualAttachmentProcessor struct { - keyPacket []byte - dataLength int - plaintextWriter io.WriteCloser - ciphertextWriter *io.PipeWriter - err error - done sync.WaitGroup -} - -// GetKeyPacket returns the key packet for the attachment. -// This should be called only after Finish() has been called. -func (ap *ManualAttachmentProcessor) GetKeyPacket() []byte { - return ap.keyPacket -} - -// GetDataLength returns the number of bytes in the DataPacket. -// This should be called only after Finish() has been called. -func (ap *ManualAttachmentProcessor) GetDataLength() int { - return ap.dataLength -} - -// Process writes attachment data to be encrypted. -func (ap *ManualAttachmentProcessor) Process(plainData []byte) error { - defer runtime.GC() - _, err := ap.plaintextWriter.Write(plainData) - return errors.Wrap(err, "gopenpgp: couldn't write attachment data") -} - -// Finish tells the processor to finalize encryption. -func (ap *ManualAttachmentProcessor) Finish() error { - defer runtime.GC() - if ap.err != nil { - return ap.err - } - if err := ap.plaintextWriter.Close(); err != nil { - return errors.Wrap(err, "gopengpp: unable to close the plaintext writer") - } - if err := ap.ciphertextWriter.Close(); err != nil { - return errors.Wrap(err, "gopengpp: unable to close the dataPacket writer") - } - ap.done.Wait() - if ap.err != nil { - return ap.err - } - return nil -} - -// NewManualAttachmentProcessor creates an AttachmentProcessor which can be used -// to encrypt a file. It takes an estimatedSize and filename as hints about the -// file and a buffer to hold the DataPacket. -// It is optimized for low-memory environments and collects garbage every megabyte. -// The buffer for the data packet must be manually allocated by the caller. -// Make sure that the dataBuffer is large enough to hold the whole data packet -// otherwise Finish() will return an error. -func (keyRing *KeyRing) NewManualAttachmentProcessor( - estimatedSize int, filename string, dataBuffer []byte, -) (*ManualAttachmentProcessor, error) { - if len(dataBuffer) == 0 { - return nil, errors.New("gopenpgp: can't give a nil or empty buffer to process the attachment") - } - - // forces the gc to be called often - debug.SetGCPercent(10) - - attachmentProc := &ManualAttachmentProcessor{} - - // hints for the encrypted file - isBinary := true - modTime := GetUnixTime() - hints := &openpgp.FileHints{ - FileName: filename, - IsBinary: isBinary, - ModTime: time.Unix(modTime, 0), - } - - // encryption config - config := &packet.Config{ - DefaultCipher: packet.CipherAES256, - Time: getTimeGenerator(), - } - - // goroutine that reads the key packet - // to be later returned to the caller via GetKeyPacket() - keyReader, keyWriter := io.Pipe() - attachmentProc.done.Add(1) - go func() { - defer attachmentProc.done.Done() - keyPacket, err := ioutil.ReadAll(keyReader) - if err != nil { - attachmentProc.err = err - } else { - attachmentProc.keyPacket = clone(keyPacket) - } - }() - - // goroutine that reads the data packet into the provided buffer - dataReader, dataWriter := io.Pipe() - attachmentProc.done.Add(1) - go func() { - defer attachmentProc.done.Done() - totalRead, err := readAll(dataBuffer, dataReader) - if err != nil { - attachmentProc.err = err - } else { - attachmentProc.dataLength = totalRead - } - }() - - // We generate the encrypting writer - var ew io.WriteCloser - var encryptErr error - ew, encryptErr = openpgp.EncryptSplit(keyWriter, dataWriter, keyRing.entities, nil, hints, config) - if encryptErr != nil { - return nil, errors.Wrap(encryptErr, "gopengpp: unable to encrypt attachment") - } - - attachmentProc.plaintextWriter = ew - attachmentProc.ciphertextWriter = dataWriter - - // The key packet should have been already written, so we can close - if err := keyWriter.Close(); err != nil { - return nil, errors.Wrap(err, "gopenpgp: couldn't close the keyPacket writer") - } - - // Check if the goroutines encountered errors - if attachmentProc.err != nil { - return nil, attachmentProc.err - } - return attachmentProc, nil -} - -// readAll works a bit like ioutil.ReadAll -// but we can choose the buffer to write to -// and we don't grow the slice in case of overflow. -func readAll(buffer []byte, reader io.Reader) (int, error) { - bufferLen := len(buffer) - totalRead := 0 - offset := 0 - overflow := false - reset := false - for { - // We read into the buffer - n, err := reader.Read(buffer[offset:]) - totalRead += n - offset += n - if !overflow && reset && n != 0 { - // In case we've started overwriting the beginning of the buffer - // We will return an error at Finish() - overflow = true - } - if err != nil { - if errors.Is(err, io.EOF) { - break - } - return 0, errors.Wrap(err, "gopenpgp: couldn't read data from the encrypted reader") - } - if offset == bufferLen { - // Here we've reached the end of the buffer - // But we need to keep reading to not block the Process() - // So we reset the buffer - reset = true - offset = 0 - } - } - if overflow { - return 0, errors.New("gopenpgp: read more bytes that was allocated in the buffer") - } - return totalRead, nil -} diff --git a/crypto/attachment_manual_test.go b/crypto/attachment_manual_test.go deleted file mode 100644 index 4a9673df..00000000 --- a/crypto/attachment_manual_test.go +++ /dev/null @@ -1,176 +0,0 @@ -package crypto - -import ( - "bytes" - "io" - "testing" - - "github.com/pkg/errors" -) - -func TestManualAttachmentProcessor(t *testing.T) { - pgp.latestServerTime = 1615394034 - defer func() { pgp.latestServerTime = testTime }() - passphrase := []byte("wUMuF/lkDPYWH/0ZqqY8kJKw7YJg6kS") - pk, err := NewKeyFromArmored(readTestFile("att_key", false)) - if err != nil { - t.Error("Expected no error while unarmoring private key, got:" + err.Error()) - } - - uk, err := pk.Unlock(passphrase) - if err != nil { - t.Error("Expected no error while unlocking private key, got:" + err.Error()) - } - - defer uk.ClearPrivateParams() - - ukr, err := NewKeyRing(uk) - if err != nil { - t.Error("Expected no error while building private keyring, got:" + err.Error()) - } - - inputPlaintext := readTestFile("att_cleartext", false) - plaintextBytes := []byte(inputPlaintext) - plaintextReader := bytes.NewReader(plaintextBytes) - bufferLen := 2 * len(plaintextBytes) - dataPacket := make([]byte, bufferLen) - ap, err := ukr.NewManualAttachmentProcessor( - len(plaintextBytes), - "test.txt", - dataPacket, - ) - if err != nil { - t.Error("Expected no error while building the attachment processor, got:" + err.Error()) - } - chunkSize := 1 << 10 - inputBytes := make([]byte, chunkSize) - var readAllPlaintext = false - for !readAllPlaintext { - nBytesRead, err := plaintextReader.Read(inputBytes) - if errors.Is(err, io.EOF) { - readAllPlaintext = true - } else if err != nil { - t.Error("Expected no error while reading plain data, got:" + err.Error()) - } - err = ap.Process(inputBytes[:nBytesRead]) - if err != nil { - t.Error("Expected no error while writing plain data, got:" + err.Error()) - } - } - err = ap.Finish() - if err != nil { - t.Error("Expected no error while calling finish, got:" + err.Error()) - } - dataLength := ap.GetDataLength() - keyPacket := ap.GetKeyPacket() - if keyPacket == nil { - t.Error("The key packet was nil") - } - if len(keyPacket) == 0 { - t.Error("The key packet was empty") - } - t.Logf("buffer size : %d total written : %d", bufferLen, dataLength) - if dataLength > bufferLen { - t.Errorf("Wrote more than was allocated, buffer size : %d total written : %d", bufferLen, dataLength) - } - - pgpMsg := NewPGPSplitMessage(keyPacket, dataPacket[:dataLength]).GetPGPMessage() - plainMsg, err := ukr.Decrypt(pgpMsg, nil, 0) - if err != nil { - t.Error("Expected no error while decrypting, got:" + err.Error()) - } - outputPlaintext := string(plainMsg.Data) - if outputPlaintext != inputPlaintext { - t.Errorf("Expectedplaintext to be %s got %s", inputPlaintext, outputPlaintext) - } -} - -func TestManualAttachmentProcessorNotEnoughBuffer(t *testing.T) { - pgp.latestServerTime = 1615394034 - defer func() { pgp.latestServerTime = testTime }() - passphrase := []byte("wUMuF/lkDPYWH/0ZqqY8kJKw7YJg6kS") - pk, err := NewKeyFromArmored(readTestFile("att_key", false)) - if err != nil { - t.Error("Expected no error while unarmoring private key, got:" + err.Error()) - } - - uk, err := pk.Unlock(passphrase) - if err != nil { - t.Error("Expected no error while unlocking private key, got:" + err.Error()) - } - - defer uk.ClearPrivateParams() - - ukr, err := NewKeyRing(uk) - if err != nil { - t.Error("Expected no error while building private keyring, got:" + err.Error()) - } - - inputPlaintext := readTestFile("att_cleartext", false) - plaintextBytes := []byte(inputPlaintext) - plaintextReader := bytes.NewReader(plaintextBytes) - bufferLen := len(plaintextBytes) / 2 - dataPacket := make([]byte, bufferLen) - ap, err := ukr.NewManualAttachmentProcessor( - len(plaintextBytes), - "test.txt", - dataPacket, - ) - if err != nil { - t.Error("Expected no error while building the attachment processor, got:" + err.Error()) - } - chunkSize := 1 << 10 - inputBytes := make([]byte, chunkSize) - var readAllPlaintext = false - for !readAllPlaintext { - nBytesRead, err := plaintextReader.Read(inputBytes) - if errors.Is(err, io.EOF) { - readAllPlaintext = true - } else if err != nil { - t.Error("Expected no error while reading plain data, got:" + err.Error()) - } - err = ap.Process(inputBytes[:nBytesRead]) - if err != nil { - t.Error("Expected no error while writing plain data, got:" + err.Error()) - } - } - err = ap.Finish() - if err == nil { - t.Error("Expected an error while calling finish, got nil") - } -} - -func TestManualAttachmentProcessorEmptyBuffer(t *testing.T) { - pgp.latestServerTime = 1615394034 - defer func() { pgp.latestServerTime = testTime }() - passphrase := []byte("wUMuF/lkDPYWH/0ZqqY8kJKw7YJg6kS") - pk, err := NewKeyFromArmored(readTestFile("att_key", false)) - if err != nil { - t.Error("Expected no error while unarmoring private key, got:" + err.Error()) - } - - uk, err := pk.Unlock(passphrase) - if err != nil { - t.Error("Expected no error while unlocking private key, got:" + err.Error()) - } - - defer uk.ClearPrivateParams() - - ukr, err := NewKeyRing(uk) - if err != nil { - t.Error("Expected no error while building private keyring, got:" + err.Error()) - } - - inputPlaintext := readTestFile("att_cleartext", false) - plaintextBytes := []byte(inputPlaintext) - bufferLen := 0 - dataPacket := make([]byte, bufferLen) - _, err = ukr.NewManualAttachmentProcessor( - len(plaintextBytes), - "test.txt", - dataPacket, - ) - if err == nil { - t.Error("Expected an error while building the attachment processor with an empty buffer got nil") - } -} diff --git a/crypto/attachment_test.go b/crypto/attachment_test.go deleted file mode 100644 index bc3334a7..00000000 --- a/crypto/attachment_test.go +++ /dev/null @@ -1,146 +0,0 @@ -package crypto - -import ( - "encoding/base64" - "testing" - - "github.com/stretchr/testify/assert" -) - -// const testAttachmentEncrypted = -// `0ksB0fHC6Duezx/0TqpK/82HSl8+qCY0c2BCuyrSFoj6Dubd93T3//32jVYa624NYvfvxX+UxFKYKJxG09gFsU1IVc87cWvUgmUmgjU=` - -var testAttachmentKey, _ = base64.StdEncoding.DecodeString("ExXmnSiQ2QCey20YLH6qlLhkY3xnIBC1AwlIXwK/HvY=") - -func TestAttachmentGetKey(t *testing.T) { - testKeyPacketsDecoded, err := base64.StdEncoding.DecodeString(readTestFile("attachment_keypacket", false)) - if err != nil { - t.Fatal("Expected no error while decoding base64 KeyPacket, got:", err) - } - - sessionKey, err := keyRingTestPrivate.DecryptSessionKey(testKeyPacketsDecoded) - if err != nil { - t.Fatal("Expected no error while decrypting KeyPacket, got:", err) - } - - assert.Exactly(t, testAttachmentKey, sessionKey.Key) -} - -func TestAttachmentSetKey(t *testing.T) { - keyPackets, err := keyRingTestPublic.EncryptSessionKey(testSessionKey) - if err != nil { - t.Fatal("Expected no error while encrypting attachment key, got:", err) - } - - sessionKey, err := keyRingTestPrivate.DecryptSessionKey(keyPackets) - if err != nil { - t.Fatal("Expected no error while decrypting attachment key, got:", err) - } - - assert.Exactly(t, testSessionKey, sessionKey) -} - -func TestAttachmentEncryptDecrypt(t *testing.T) { - var testAttachmentCleartext = "cc,\ndille." - var message = NewPlainMessageFromFile([]byte(testAttachmentCleartext), "test.txt", 1602518992) - - encSplit, err := keyRingTestPrivate.EncryptAttachment(message, "") - if err != nil { - t.Fatal("Expected no error while encrypting attachment, got:", err) - } - - redecData, err := keyRingTestPrivate.DecryptAttachment(encSplit) - if err != nil { - t.Fatal("Expected no error while decrypting attachment, got:", err) - } - - assert.Exactly(t, message, redecData) -} - -func TestAttachmentEncrypt(t *testing.T) { - var testAttachmentCleartext = "cc,\ndille." - var message = NewPlainMessageFromFile([]byte(testAttachmentCleartext), "test.txt", 1602518992) - - encSplit, err := keyRingTestPrivate.EncryptAttachment(message, "") - if err != nil { - t.Fatal("Expected no error while encrypting attachment, got:", err) - } - - pgpMessage := NewPGPMessage(encSplit.GetBinary()) - - redecData, err := keyRingTestPrivate.Decrypt(pgpMessage, nil, 0) - if err != nil { - t.Fatal("Expected no error while decrypting attachment, got:", err) - } - - assert.Exactly(t, message, redecData) -} - -func TestAttachmentDecrypt(t *testing.T) { - var testAttachmentCleartext = "cc,\ndille." - var message = NewPlainMessageFromFile([]byte(testAttachmentCleartext), "test.txt", 1602518992) - - encrypted, err := keyRingTestPrivate.Encrypt(message, nil) - if err != nil { - t.Fatal("Expected no error while encrypting attachment, got:", err) - } - - armored, err := encrypted.GetArmored() - if err != nil { - t.Fatal("Expected no error while armoring, got:", err) - } - - pgpSplitMessage, err := NewPGPSplitMessageFromArmored(armored) - if err != nil { - t.Fatal("Expected no error while unarmoring, got:", err) - } - - redecData, err := keyRingTestPrivate.DecryptAttachment(pgpSplitMessage) - if err != nil { - t.Fatal("Expected no error while decrypting attachment, got:", err) - } - - assert.Exactly(t, message, redecData) -} - -func TestAttachmentDecryptStatic(t *testing.T) { - passphrase := []byte("wUMuF/lkDPYWH/0ZqqY8kJKw7YJg6kS") - keyPacket, err := base64.StdEncoding.DecodeString(readTestFile("att_keypacket", false)) - if err != nil { - t.Error("Expected no error while decoding key packet, got:" + err.Error()) - } - - dataPacket, err := base64.StdEncoding.DecodeString(readTestFile("att_body", false)) - if err != nil { - t.Error("Expected no error while decoding data packet, got:" + err.Error()) - } - - pk, err := NewKeyFromArmored(readTestFile("att_key", false)) - if err != nil { - t.Error("Expected no error while unarmoring private key, got:" + err.Error()) - } - - uk, err := pk.Unlock(passphrase) - if err != nil { - t.Error("Expected no error while unlocking private key, got:" + err.Error()) - } - - defer uk.ClearPrivateParams() - - ukr, err := NewKeyRing(uk) - if err != nil { - t.Error("Expected no error while building private keyring, got:" + err.Error()) - } - - pgpSplitMessage := NewPGPSplitMessage(keyPacket, dataPacket) - if err != nil { - t.Fatal("Expected no error while unarmoring, got:", err) - } - - dec, err := ukr.DecryptAttachment(pgpSplitMessage) - if err != nil { - t.Fatal("Expected no error while decrypting attachment, got:", err) - } - - assert.Exactly(t, []byte("PNG"), dec.GetBinary()[1:4]) -} diff --git a/crypto/base_test.go b/crypto/base_test.go index 72c92c16..454f885e 100644 --- a/crypto/base_test.go +++ b/crypto/base_test.go @@ -2,21 +2,26 @@ package crypto import ( "crypto/rsa" - "io/ioutil" "math/big" + "os" "strings" "testing" "github.com/ProtonMail/go-crypto/openpgp/ecdh" "github.com/ProtonMail/go-crypto/openpgp/eddsa" + "github.com/ProtonMail/gopenpgp/v3/profile" "github.com/stretchr/testify/assert" ) const testTime = 1557754627 // 2019-05-13T13:37:07+00:00 +const testMessage = "Hello world!" + +var testPGP *PGPHandle +var testProfiles []*profile.Custom func readTestFile(name string, trimNewlines bool) string { - data, err := ioutil.ReadFile("testdata/" + name) //nolint + data, err := os.ReadFile("testdata/" + name) //nolint if err != nil { panic(err) } @@ -27,8 +32,11 @@ func readTestFile(name string, trimNewlines bool) string { } func init() { - UpdateTime(testTime) // 2019-05-13T13:37:07+00:00 + testPGP = PGP() + testPGP.defaultTime = NewConstantClock(testTime) // 2019-05-13T13:37:07+00:00 + testProfiles = []*profile.Custom{profile.Default(), profile.RFC4880(), profile.RFC9580()} + initEncDecTest() initGenerateKeys() initArmoredKeys() initKeyRings() diff --git a/crypto/crypto.go b/crypto/crypto.go new file mode 100644 index 00000000..0ca88e7d --- /dev/null +++ b/crypto/crypto.go @@ -0,0 +1,79 @@ +// Package crypto provides a high-level API for common OpenPGP functionality. +// The package provides abstract interfaces for encryption ([PGPEncryption]), +// decryption ([PGPDecryption]), signing ([PGPSign]), and verifying ([PGPVerify]). +// +// # Usage +// +// To get a concrete instantiation of the interfaces use the top level [PGPHandle] by +// calling PGP() or PGPWithProfile(...). An example to instantiate a handle +// that implements [PGPEncryption]: +// +// pgp := PGP() +// encryptionHandle, _ :=pgp.Encryption().Password(...).New() +package crypto + +import ( + "time" + + "github.com/ProtonMail/gopenpgp/v3/profile" +) + +type PGPHandle struct { + profile *profile.Custom + defaultTime Clock +} + +// PGP creates a PGPHandle to interact with the API. +// Uses the default profile for configuration. +func PGP() *PGPHandle { + return PGPWithProfile(profile.Default()) +} + +// PGPWithProfile creates a PGPHandle to interact with the API. +// Uses the provided profile for configuration. +func PGPWithProfile(profile *profile.Custom) *PGPHandle { + return &PGPHandle{ + profile: profile, + defaultTime: time.Now, + } +} + +// Encryption returns a builder to create an EncryptionHandle +// for encrypting messages. +func (p *PGPHandle) Encryption() *EncryptionHandleBuilder { + return newEncryptionHandleBuilder(p.profile, p.defaultTime) +} + +// Decryption returns a builder to create a DecryptionHandle +// for decrypting pgp messages. +func (p *PGPHandle) Decryption() *DecryptionHandleBuilder { + return newDecryptionHandleBuilder(p.profile, p.defaultTime) +} + +// Sign returns a builder to create a SignHandle +// for signing messages. +func (p *PGPHandle) Sign() *SignHandleBuilder { + return newSignHandleBuilder(p.profile, p.defaultTime) +} + +// Verify returns a builder to create an VerifyHandle +// for verifying signatures. +func (p *PGPHandle) Verify() *VerifyHandleBuilder { + return newVerifyHandleBuilder(p.profile, p.defaultTime) +} + +// KeyGeneration returns a builder to create a KeyGeneration handle. +func (p *PGPHandle) KeyGeneration() *KeyGenerationBuilder { + return newKeyGenerationBuilder(p.profile, p.defaultTime) +} + +// LockKey encrypts the private parts of a copy of the input key with the given passphrase. +func (p *PGPHandle) LockKey(key *Key, passphrase []byte) (*Key, error) { + return key.lock(passphrase, p.profile) +} + +// GenerateSessionKey generates a random session key for the profile. +func (p *PGPHandle) GenerateSessionKey() (*SessionKey, error) { + config := p.profile.EncryptionConfig() + return generateSessionKey(config) +} diff --git a/crypto/crypto_example_test.go b/crypto/crypto_example_test.go new file mode 100644 index 00000000..18767071 --- /dev/null +++ b/crypto/crypto_example_test.go @@ -0,0 +1,785 @@ +package crypto + +import ( + "bytes" + "encoding/hex" + "fmt" + "io" + "strings" + + "github.com/ProtonMail/gopenpgp/v3/constants" + "github.com/ProtonMail/gopenpgp/v3/profile" +) + +const examplePubKey = `-----BEGIN PGP PUBLIC KEY BLOCK----- + +xiYEZIbSkxsHknQrXGfb+kM2iOsOvin8yE05ff5hF8KE6k+saspAZc0VdXNlciA8 +dXNlckB0ZXN0LnRlc3Q+wocEExsIAD0FAmSG0pMJkEHsytogdrSJFiEEamc2vcEG +XMMaYxmDQezK2iB2tIkCGwMCHgECGQECCwcCFQgCFgADJwcCAABTnme46ymbAs0X +7tX3xWu+9O+LLdM0aAUyV6FwUNWcy47IfmTunwdqHZ2CbUGLLb+OR/9yci1aIHDJ +xXmJh3kj9wDOJgRkhtKTGX6Xe04jkL+7ikivpOB0/ZSq+fnZr2+76Mf/InbOrpxJ +wnQEGBsIACoFAmSG0pMJkEHsytogdrSJFiEEamc2vcEGXMMaYxmDQezK2iB2tIkC +GwwAAMJizYj3AFqQi70eHGzhHcmr0XwnsAfLGw0vQaiZn6HGITQw5nBGvXQPF9Vp +FpsXV9x/08dIdfZLAQVdQowgeBsxCw== +=JIkN +-----END PGP PUBLIC KEY BLOCK-----` + +const examplePrivKey = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xUkEZIbSkxsHknQrXGfb+kM2iOsOvin8yE05ff5hF8KE6k+saspAZQCy/kfFUYc2 +GkpOHc42BI+MsysKzk4ofjBAfqM+bb7goQ3hzRV1c2VyIDx1c2VyQHRlc3QudGVz +dD7ChwQTGwgAPQUCZIbSkwmQQezK2iB2tIkWIQRqZza9wQZcwxpjGYNB7MraIHa0 +iQIbAwIeAQIZAQILBwIVCAIWAAMnBwIAAFOeZ7jrKZsCzRfu1ffFa77074st0zRo +BTJXoXBQ1ZzLjsh+ZO6fB2odnYJtQYstv45H/3JyLVogcMnFeYmHeSP3AMdJBGSG +0pMZfpd7TiOQv7uKSK+k4HT9lKr5+dmvb7vox/8ids6unEkAF1v8fCKogIrtBWVT +nVbwnovjM3LLexpXFZSgTKRcNMgPRMJ0BBgbCAAqBQJkhtKTCZBB7MraIHa0iRYh +BGpnNr3BBlzDGmMZg0HsytogdrSJAhsMAADCYs2I9wBakIu9Hhxs4R3Jq9F8J7AH +yxsNL0GomZ+hxiE0MOZwRr10DxfVaRabF1fcf9PHSHX2SwEFXUKMIHgbMQs= +=bJqd +-----END PGP PRIVATE KEY BLOCK-----` + +const exampleEncryptedMessagePw = `-----BEGIN PGP MESSAGE----- + +wy4ECQMIP4yfOrrWtD/g8JituG2N1xeV1o6Frdc3yn7JRoUMv0E69MyI/ito/OH2 +0jsBi1NQPKaykn/nSxv+6xzAEWw2lZ6g6hy/gAGgbYy64Fkh652WMvjO9KKDZycB +orleBckXgArj64+2Kw== +-----END PGP MESSAGE-----` + +const exampleEncryptedMessagePub = `-----BEGIN PGP MESSAGE----- + +wUQDYc6clYlCdtoZPfr2cdbY5lVXC+J7/4CpjCocMlEqs739c5cu6rwx8yUZByKm +a/HB5JEf9oIsN/ekyHTmcftfTetwiNI7AfuPTGQYB3/PtamMToIClX5Ca+eDb6iU +VS0r4gpTv5iIvl9+k4sbSvxwjNMLBy9DyB+mlFc3OZ5dtFk= +-----END PGP MESSAGE-----` + +const exampleEncryptedAndSigned = `-----BEGIN PGP MESSAGE----- + +wUQDYc6clYlCdtoZgMQH5tynaMh60pY+uo3KuXzCM+bDkO1VqrL5IBRQWWMZB/2r +H8be1jZayJ8a1F+FG6Xs+LO2INR4lNLAFAHoLWu2En755DDZJwQwnDQ6Gywq26aq +STVC0Bt+srqxxOKJJFA2lN4tlVsn1pKsgaqO6s52JDFlT0OijF8wgz/kfc/ZwT7Z +EBdqURMaF3wUlQ5nX2/ZDQJNfU/d79W0+8IQ7QrVhVQuV7sub7EbpqSuFTwqPt73 +jsLFlYxlYsgaKCHsmVVJWb1uIdPzHXXajTVPa1aRkkyCYMayXJEWOu7+HoW0Ipk1 +piIFyQpQFvj9RAwuKaGQUfb5KRP1fegjwZd0/8TWDfLPuD2Fc3LBicxpI1Hk +-----END PGP MESSAGE-----` + +const exampleDetachedSignature = `-----BEGIN PGP SIGNATURE----- + +wnEEABsIACcFAmSe4bEJkEHsytogdrSJFiEEamc2vcEGXMMaYxmDQezK2iB2tIkA +AIL40zuQXGBliZqje7eniv34NFZf2zQIE8gQOdcnWqFLlgg210RHjqqS2qGOuQyL +exN6Dbkaubvk6EhvTmYXIXnSAQ== +=EeGr +-----END PGP SIGNATURE-----` + +const exampleInlineSignature = `-----BEGIN PGP MESSAGE----- + +xA0DAAgbQezK2iB2tIkByxViAAAAAABtZXNzYWdlIHRvIHNpZ27CcQQAGwgAJwUC +ZJ7jVQmQQezK2iB2tIkWIQRqZza9wQZcwxpjGYNB7MraIHa0iQAA/+K//wXxUyVf +m50qAP92QZTqsHIokut9xP8Lp/ntSdKWBLdmoWHVpXElGpnIinSNt6NNjHj+S22u +QqO+M5PHk0cI +-----END PGP MESSAGE-----` + +const exampleCleartextSignature = `-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA512 + +message to sign +-----BEGIN PGP SIGNATURE----- + +wnEEARsKACcFAmSe5K0JkEHsytogdrSJFiEEamc2vcEGXMMaYxmDQezK2iB2tIkA +APwuHaxUQ7xX4WdqWm7WnipmbM/ARTJPESACNoFlw7p/aHuXw+nyolUeIRnadyle +0KepPKwDaTY+7Jk7/5kv7NiqAg== +-----END PGP SIGNATURE-----` + +const exampleSplitMessage = `c1440361ce9c95894276da19cf0760b3a1150038c6d9ab20a5594fc9e32ce12009fb0a3ec12783b8efcf57521907d012786468567d736db82a9b160a598ea8decd762b982063 +d2c0140104f3439588864ca36d6c15c0ae9364c706a869f13fc71987acd5061716914b03b4ef1884d67d19f28e9c29447fbd76781cd9b69bad22fc2b7eefd0d1b6c4e5d3f90368d19b5a2eb02cb1fb4d706f77feb1b200ac553cd872e1e695bafbac39fbf729f89a96aaf9fdef72c801545db2e627b357df18d05841f2fbd5aeb82b38db28a7f4cd946b17f98922fcbd78cf03b3ff7247918f381e61482960a9eec2192c64aa1a3eddbab486a7372c65e8f2c9b284f6b232cd3a4147fa374635cd1ad7e8b210334fce25c49cce99f91ff835dbfb3c6a27` + +const exampleEncryptedDetachedMessage = `-----BEGIN PGP MESSAGE----- + +wVQDYc6clYlCdtoZVaZe8pDekqVSnY9/wtXIPV92Yi1b/Nc0cxaw3CyG7xkpCbnc +V5NWsbpp0NaJ3Gxq/APdetC3iPG+AjM4xuWKhZWZ3/+bea/2q8jSOwF43weMcuQF +zXxGfqB9uLYsOXejBTO4oPDbuWH11SVibxa6k1X79l2+kf2dDgruhMk564h4SU6v +dbID +-----END PGP MESSAGE-----` + +const exampleEncryptedDetachedSignatureMessage = `-----BEGIN PGP MESSAGE----- + +wVQDYc6clYlCdtoZVaZe8pDekqVSnY9/wtXIPV92Yi1b/Nc0cxaw3CyG7xkpCbnc +V5NWsbpp0NaJ3Gxq/APdetC3iPG+AjM4xuWKhZWZ3/+bea/2q8jSpAHHbawcWmFq +ecrYdNNCv7KZS3ofScFXVuYWI8tc8sgaCUd2b82krn0dWRAzaxBuz+rJ99/jQa8U +GZSc+PaljzEvgR+GoQc5h8VJ58UmDXoN9VhWYKBBz1B1bkCu1vsDNS1yXk/XNi2e +BMEdvkP9hUOsJTO9e1qQLhmmEQlyHkmQGSyIHFNaYMI18RSUVmfZwZ7fsL/I07zp +nNWmL4dHU3Ba+56V +-----END PGP MESSAGE-----` + +func ExamplePGPHandle_Encryption_password() { + // Encrypt data with a password + password := []byte("hunter2") + pgp := PGP() + encHandle, err := pgp.Encryption(). + Password(password). + New() + if err != nil { + return + } + pgpMessage, err := encHandle.Encrypt([]byte("my message")) + if err != nil { + return + } + armored, err := pgpMessage.Armor() + if err != nil { + return + } + fmt.Println(armored) +} + +func ExamplePGPHandle_Encryption_asymmetric() { + // Encrypt data with a public key + publicKey, err := NewKeyFromArmored(examplePubKey) + if err != nil { + return + } + pgp := PGP() + encHandle, err := pgp.Encryption(). + Recipient(publicKey). + New() + if err != nil { + return + } + pgpMessage, err := encHandle.Encrypt([]byte("my message")) + if err != nil { + return + } + armored, err := pgpMessage.Armor() + if err != nil { + return + } + fmt.Println(armored) +} + +func ExamplePGPHandle_Encryption_signcrypt() { + // Encrypt data with a public key + // and sign with private key + publicKey, err := NewKeyFromArmored(examplePubKey) + if err != nil { + return + } + privateKey, err := NewKeyFromArmored(examplePrivKey) + if err != nil { + return + } + defer privateKey.ClearPrivateParams() + pgp := PGP() + encHandle, err := pgp.Encryption(). + Recipient(publicKey). + SigningKey(privateKey). + New() + if err != nil { + return + } + pgpMessage, err := encHandle.Encrypt([]byte("my message")) + if err != nil { + return + } + armored, err := pgpMessage.Armor() + if err != nil { + return + } + fmt.Println(armored) +} + +func ExamplePGPHandle_Encryption_stream() { + // Encrypt data with a public key + // and sign with private key streaming + publicKey, err := NewKeyFromArmored(examplePubKey) + if err != nil { + return + } + privateKey, err := NewKeyFromArmored(examplePrivKey) + if err != nil { + return + } + defer privateKey.ClearPrivateParams() + pgp := PGP() + encHandle, err := pgp.Encryption(). + Recipient(publicKey). + SigningKey(privateKey). + New() + if err != nil { + return + } + messageReader := strings.NewReader("my message") + var ciphertextWriter bytes.Buffer + ptWriter, err := encHandle.EncryptingWriter(&ciphertextWriter, Armor) + if err != nil { + fmt.Println(err) + return + } + if _, err = io.Copy(ptWriter, messageReader); err != nil { + return + } + if err = ptWriter.Close(); err != nil { + return + } + fmt.Println(ciphertextWriter.String()) +} + +func ExamplePGPHandle_Encryption_split() { + // Split encrypted message into key packets and data packets. + publicKey, err := NewKeyFromArmored(examplePubKey) + if err != nil { + return + } + privateKey, err := NewKeyFromArmored(examplePrivKey) + if err != nil { + return + } + defer privateKey.ClearPrivateParams() + pgp := PGP() + encHandle, err := pgp.Encryption(). + Recipient(publicKey). + SigningKey(privateKey). + New() + if err != nil { + return + } + var keyPackets bytes.Buffer + var dataPackets bytes.Buffer + splitWriter := NewPGPSplitWriterKeyAndData(&keyPackets, &dataPackets) + ptWriter, err := encHandle.EncryptingWriter(splitWriter, Bytes) + if err != nil { + return + } + if _, err = io.Copy(ptWriter, strings.NewReader("my message")); err != nil { + return + } + if err = ptWriter.Close(); err != nil { + return + } + fmt.Printf("%x\n", keyPackets.Bytes()) + fmt.Printf("%x\n", dataPackets.Bytes()) +} + +func ExamplePGPHandle_Encryption_detached() { + // Produce encrypted detached signatures instead of + // embedded signatures: + publicKey, err := NewKeyFromArmored(examplePubKey) + if err != nil { + return + } + privateKey, err := NewKeyFromArmored(examplePrivKey) + if err != nil { + return + } + defer privateKey.ClearPrivateParams() + pgp := PGP() + encHandle, err := pgp.Encryption(). + Recipient(publicKey). + SigningKey(privateKey). + DetachedSignature(). + New() + if err != nil { + return + } + var pgpMessage bytes.Buffer + var pgpSignatureMessage bytes.Buffer + splitWriter := NewPGPSplitWriterDetachedSignature(&pgpMessage, &pgpSignatureMessage) + ptWriter, err := encHandle.EncryptingWriter(splitWriter, Armor) + if err != nil { + return + } + if _, err = io.Copy(ptWriter, strings.NewReader("my message")); err != nil { + return + } + if err = ptWriter.Close(); err != nil { + return + } + fmt.Println(pgpMessage.String()) + fmt.Println(pgpSignatureMessage.String()) +} + +func ExamplePGPHandle_Decryption_password() { + // Decrypt data with a password + pgp := PGP() + decHandle, err := pgp. + Decryption(). + Password([]byte("hunter2")). + New() + if err != nil { + fmt.Println(err) + return + } + decrypted, err := decHandle.Decrypt([]byte(exampleEncryptedMessagePw), Armor) + if err != nil { + fmt.Println(err) + return + } + fmt.Println(string(decrypted.Bytes())) + // Output: my message +} + +func ExamplePGPHandle_Decryption_asymmetric() { + // Decrypt armored encrypted message using + // the private key and obtain the plaintext + privateKey, err := NewKeyFromArmored(examplePrivKey) + if err != nil { + fmt.Println(err) + return + } + defer privateKey.ClearPrivateParams() + pgp := PGP() + decHandle, err := pgp. + Decryption(). + DecryptionKey(privateKey). + New() + if err != nil { + fmt.Println(err) + return + } + decrypted, err := decHandle.Decrypt([]byte(exampleEncryptedMessagePub), Armor) + if err != nil { + fmt.Println(err) + return + } + fmt.Println(decrypted.String()) + // Output: my message +} + +func ExamplePGPHandle_Decryption_signcrypt() { + // Decrypt armored encrypted message using + // the private key and obtain the plaintext + publicKey, err := NewKeyFromArmored(examplePubKey) + if err != nil { + fmt.Println(err) + return + } + privateKey, err := NewKeyFromArmored(examplePrivKey) + if err != nil { + fmt.Println(err) + return + } + defer privateKey.ClearPrivateParams() + pgp := PGP() + decHandle, err := pgp. + Decryption(). + DecryptionKey(privateKey). + VerificationKey(publicKey). + New() + if err != nil { + fmt.Println(err) + return + } + decrypted, err := decHandle.Decrypt([]byte(exampleEncryptedAndSigned), Armor) + if err != nil { + return + } + if sigErr := decrypted.SignatureError(); sigErr != nil { + fmt.Println(sigErr) + return + } else { + fmt.Println("OK") + } + fmt.Println(decrypted.String()) + // Output: OK + // my message +} + +func ExamplePGPHandle_Decryption_split() { + // Decrypt key and data packet + // separately. + data := strings.Split(exampleSplitMessage, "\n") + keyPacket, _ := hex.DecodeString(data[0]) + dataPacket, _ := hex.DecodeString(data[1]) + publicKey, err := NewKeyFromArmored(examplePubKey) + if err != nil { + fmt.Println(err) + return + } + privateKey, err := NewKeyFromArmored(examplePrivKey) + if err != nil { + fmt.Println(err) + return + } + defer privateKey.ClearPrivateParams() + pgp := PGP() + decHandle, err := pgp. + Decryption(). + DecryptionKey(privateKey). + New() + if err != nil { + fmt.Println(err) + return + } + sessionKey, err := decHandle.DecryptSessionKey(keyPacket) + if err != nil { + fmt.Println(err) + return + } + decHandle, err = pgp. + Decryption(). + SessionKey(sessionKey). + VerificationKey(publicKey). + DisableIntendedRecipients(). + New() + if err != nil { + fmt.Println(err) + return + } + decrypted, err := decHandle.Decrypt(dataPacket, Bytes) + if err != nil { + fmt.Println(err) + return + } + if sigErr := decrypted.SignatureError(); sigErr != nil { + fmt.Println(sigErr) + return + } else { + fmt.Println("OK") + } + fmt.Println(decrypted.String()) + // Output: OK + // my message +} + +func ExamplePGPHandle_Decryption_stream() { + // Decrypt armored encrypted message using + // the private key and obtain the plaintext with streaming + publicKey, err := NewKeyFromArmored(examplePubKey) + if err != nil { + return + } + privateKey, err := NewKeyFromArmored(examplePrivKey) + if err != nil { + return + } + defer privateKey.ClearPrivateParams() + pgp := PGP() + decHandle, err := pgp. + Decryption(). + DecryptionKey(privateKey). + VerificationKey(publicKey). + New() + if err != nil { + fmt.Println(err) + return + } + ciphertextReader := strings.NewReader(exampleEncryptedAndSigned) + ptReader, err := decHandle.DecryptingReader(ciphertextReader, Armor) + if err != nil { + fmt.Println(err) + return + } + decrypted, err := ptReader.ReadAllAndVerifySignature() + if err != nil { + fmt.Println(err) + return + } + if sigErr := decrypted.SignatureError(); sigErr != nil { + fmt.Println(sigErr) + return + } else { + fmt.Println("OK") + } + fmt.Println(decrypted.String()) + // Output: OK + // my message +} + +func ExamplePGPHandle_Decryption_detached() { + // Decrypt armored encrypted message and verify an encrypted + // detached signature. + publicKey, err := NewKeyFromArmored(examplePubKey) + if err != nil { + return + } + privateKey, err := NewKeyFromArmored(examplePrivKey) + if err != nil { + return + } + defer privateKey.ClearPrivateParams() + pgp := PGP() + decHandle, err := pgp. + Decryption(). + DecryptionKey(privateKey). + VerificationKey(publicKey). + New() + if err != nil { + fmt.Println(err) + return + } + ciphertextReader := NewPGPSplitReader( + strings.NewReader(exampleEncryptedDetachedMessage), + strings.NewReader(exampleEncryptedDetachedSignatureMessage), + ) + ptReader, err := decHandle.DecryptingReader(ciphertextReader, Armor) + if err != nil { + fmt.Println(err) + return + } + decrypted, err := ptReader.ReadAllAndVerifySignature() + if err != nil { + fmt.Println(err) + return + } + if sigErr := decrypted.SignatureError(); sigErr != nil { + fmt.Println(sigErr) + return + } else { + fmt.Println("OK") + } + fmt.Println(decrypted.String()) + // Output: OK + // my message +} + +func ExamplePGPHandle_KeyGeneration_basic() { + pgp := PGP() + // Generate a PGP key + genHandle := pgp.KeyGeneration(). + AddUserId("Max Mustermann", "max.mustermann@example.com"). + New() + key, err := genHandle.GenerateKey() + if err != nil { + return + } + fmt.Println(key.Armor()) +} + +func ExamplePGPHandle_KeyGeneration_profile() { + // Generate a PGP key with the crypto-refresh profile + pgp := PGPWithProfile(profile.RFC9580()) + genHandle := pgp.KeyGeneration(). + AddUserId("Max Mustermann", "max.mustermann@example.com"). + New() + key, err := genHandle.GenerateKey() + if err != nil { + return + } + fmt.Println(key.Armor()) +} + +func ExamplePGPHandle_KeyGeneration_level() { + // Generate a PGP key with the crypto-refresh profile + // higher security level (Curve448) + pgp := PGPWithProfile(profile.RFC9580()) + genHandle := pgp.KeyGeneration(). + AddUserId("Max Mustermann", "max.mustermann@example.com"). + New() + key, err := genHandle.GenerateKeyWithSecurity(constants.HighSecurity) + if err != nil { + return + } + fmt.Println(key.Armor()) +} + +func ExamplePGPHandle_Sign_detached() { + // Sign a plaintext with a private key + // using a detached signatures. + privateKey, err := NewKeyFromArmored(examplePrivKey) + if err != nil { + return + } + defer privateKey.ClearPrivateParams() + pgp := PGP() + signingMessage := []byte("message to sign") + signer, _ := pgp.Sign(). + SigningKey(privateKey). + Detached(). + New() + signature, err := signer.Sign(signingMessage, Armor) + if err != nil { + return + } + fmt.Println(string(signature)) +} + +func ExamplePGPHandle_Sign_inline() { + // Sign a plaintext with a private key + // using a inline signature. + privateKey, err := NewKeyFromArmored(examplePrivKey) + if err != nil { + return + } + defer privateKey.ClearPrivateParams() + pgp := PGP() + signingMessage := []byte("message to sign") + signer, _ := pgp.Sign(). + SigningKey(privateKey). + New() + signatureMessage, err := signer.Sign(signingMessage, Armor) + if err != nil { + return + } + fmt.Println(string(signatureMessage)) +} + +func ExamplePGPHandle_Sign_cleartext() { + // Sign a plaintext with a private key + // using the cleartext signature framework. + privateKey, err := NewKeyFromArmored(examplePrivKey) + if err != nil { + return + } + defer privateKey.ClearPrivateParams() + pgp := PGP() + signingMessage := []byte("message to sign") + signer, _ := pgp.Sign(). + SigningKey(privateKey). + New() + signatureMessage, err := signer.SignCleartext(signingMessage) + if err != nil { + return + } + fmt.Println(string(signatureMessage)) +} + +func ExamplePGPHandle_Sign_stream() { + // Sign a plaintext with a private key + // using a inline signature with streaming. + privateKey, err := NewKeyFromArmored(examplePrivKey) + if err != nil { + return + } + defer privateKey.ClearPrivateParams() + pgp := PGP() + signer, _ := pgp.Sign(). + SigningKey(privateKey). + New() + var signedMessage bytes.Buffer + messageWriter, err := signer.SigningWriter(&signedMessage, Armor) + if err != nil { + return + } + if _, err = io.Copy(messageWriter, strings.NewReader("message to sign")); err != nil { + return + } + if err = messageWriter.Close(); err != nil { + return + } + fmt.Println(signedMessage.String()) +} + +func ExamplePGPHandle_Verify_detached() { + // Verify detached signature with a public key. + verifyMessage := []byte("message to sign") + publicKey, err := NewKeyFromArmored(examplePubKey) + if err != nil { + fmt.Println(err) + return + } + pgp := PGP() + verifier, _ := pgp.Verify(). + VerificationKey(publicKey). + New() + verifyResult, err := verifier.VerifyDetached(verifyMessage, []byte(exampleDetachedSignature), Armor) + if err != nil { + fmt.Println(err) + return + } + if sigErr := verifyResult.SignatureError(); sigErr != nil { + fmt.Println(sigErr) + } else { + fmt.Println("OK") + } + // Output: OK +} + +func ExamplePGPHandle_Verify_inline() { + // Verify a inline signed message with a public key. + publicKey, err := NewKeyFromArmored(examplePubKey) + if err != nil { + fmt.Println(err) + return + } + pgp := PGP() + verifier, _ := pgp.Verify(). + VerificationKey(publicKey). + New() + verifyResult, err := verifier.VerifyInline([]byte(exampleInlineSignature), Armor) + if err != nil { + fmt.Println(err) + return + } + if sigErr := verifyResult.SignatureError(); sigErr != nil { + fmt.Println(sigErr) + } else { + fmt.Println("OK") + } + fmt.Println(verifyResult.String()) + // Output: OK + // message to sign +} + +func ExamplePGPHandle_Verify_cleartext() { + // Verify a cleartext signed message with a public key. + publicKey, err := NewKeyFromArmored(examplePubKey) + if err != nil { + fmt.Println(err) + return + } + pgp := PGP() + verifier, _ := pgp.Verify(). + VerificationKey(publicKey). + New() + verifyResult, err := verifier.VerifyCleartext([]byte(exampleCleartextSignature)) + if err != nil { + fmt.Println(err) + return + } + if sigErr := verifyResult.SignatureError(); sigErr != nil { + fmt.Println(sigErr) + } else { + fmt.Println("OK") + } + fmt.Println(string(verifyResult.Cleartext())) + // Output: OK + // message to sign +} + +func ExamplePGPHandle_Verify_stream() { + // Verify a inline signed message with a public key using streaming. + publicKey, err := NewKeyFromArmored(examplePubKey) + if err != nil { + fmt.Println(err) + return + } + pgp := PGP() + verifier, _ := pgp.Verify(). + VerificationKey(publicKey). + New() + messageReader, err := verifier.VerifyingReader(nil, strings.NewReader(exampleInlineSignature), Armor) + if err != nil { + fmt.Println(err) + return + } + verifyResult, err := messageReader.ReadAllAndVerifySignature() + if err != nil { + fmt.Println(err) + return + } + if sigErr := verifyResult.SignatureError(); sigErr != nil { + fmt.Println(sigErr) + } else { + fmt.Println("OK") + } + fmt.Println(verifyResult.String()) + // Output: OK + // message to sign +} + +func ExamplePGPHandle_LockKey() { + // Encrypt secret material in a private key with a passphrase. + privateKey, err := NewKeyFromArmored(examplePrivKey) + if err != nil { + fmt.Println(err) + return + } + defer privateKey.ClearPrivateParams() + pgp := PGP() + lockedKey, err := pgp.LockKey(privateKey, []byte("password")) + if err != nil { + fmt.Println(err) + return + } + locked, _ := lockedKey.IsLocked() + fmt.Println(locked) + // Output: true +} diff --git a/crypto/decryption.go b/crypto/decryption.go new file mode 100644 index 00000000..7a065192 --- /dev/null +++ b/crypto/decryption.go @@ -0,0 +1,42 @@ +package crypto + +// PGPDecryption is an interface for decrypting pgp messages with GopenPGP. +// Use the DecryptionHandleBuilder to create a handle that implements PGPDecryption. +type PGPDecryption interface { + // DecryptingReader returns a wrapper around underlying encryptedMessage Reader, + // such that any read-operation via the wrapper results in a read from the decrypted pgp message. + // The returned VerifyDataReader has to be fully read before any potential signatures can be verified. + // Either read the message fully end then call VerifySignature or use the helper method ReadAllAndVerifySignature. + // The encoding indicates if the input message should be unarmored or not, i.e., Bytes/Armor/Auto + // where Auto tries to detect automatically. + // If encryptedMessage is of type PGPSplitReader, the method tries to verify an encrypted detached signature + // that is read from the separate reader. + DecryptingReader(encryptedMessage Reader, encoding int8) (*VerifyDataReader, error) + // Decrypt decrypts an encrypted pgp message. + // Returns a VerifiedDataResult, which can be queried for potential signature verification errors, + // and the plaintext data. Note that on a signature error, the method does not return an error. + // Instead, the signature error is stored within the VerifiedDataResult. + // The encoding indicates if the input message should be unarmored or not, i.e., Bytes/Armor/Auto + // where Auto tries to detect automatically. + Decrypt(pgpMessage []byte, encoding int8) (*VerifiedDataResult, error) + // DecryptDetached provides the same functionality as Decrypt but allows + // to supply an encrypted detached signature that should be decrypted and verified + // against the data in the pgp message. If encDetachedSignature is nil, the behavior is similar + // to Decrypt. The encoding indicates if the input message should be unarmored or not, + // i.e., Bytes/Armor/Auto where Auto tries to detect automatically. + DecryptDetached(pgpMessage []byte, encDetachedSignature []byte, encoding int8) (*VerifiedDataResult, error) + // DecryptSessionKey decrypts an encrypted session key. + // To decrypt a session key, the decryption handle must contain either a decryption key or a password. + DecryptSessionKey(keyPackets []byte) (*SessionKey, error) + // ClearPrivateParams clears all private key material contained in EncryptionHandle from memory. + ClearPrivateParams() +} + +type Reader interface { + Read(b []byte) (n int, err error) +} + +type PGPSplitReader interface { + Reader + Signature() Reader +} diff --git a/crypto/decryption_core.go b/crypto/decryption_core.go new file mode 100644 index 00000000..a4c20b99 --- /dev/null +++ b/crypto/decryption_core.go @@ -0,0 +1,344 @@ +package crypto + +import ( + "bytes" + "io" + + "github.com/ProtonMail/go-crypto/openpgp/packet" + openpgp "github.com/ProtonMail/go-crypto/openpgp/v2" + "github.com/ProtonMail/gopenpgp/v3/constants" + "github.com/ProtonMail/gopenpgp/v3/internal" + "github.com/pkg/errors" +) + +type pgpSplitReader struct { + encMessage, encSignature Reader +} + +// pgpSplitReader implements the PGPSplitReader interface + +func (mw *pgpSplitReader) Read(b []byte) (int, error) { + return mw.encMessage.Read(b) +} + +func (mw *pgpSplitReader) Signature() Reader { + return mw.encSignature +} + +func NewPGPSplitReader(pgpMessage Reader, pgpEncryptedSignature Reader) *pgpSplitReader { + return &pgpSplitReader{ + encMessage: pgpMessage, + encSignature: pgpEncryptedSignature, + } +} + +// decryptStream decrypts the stream either with the secret keys or a password. +func (dh *decryptionHandle) decryptStream(encryptedMessage Reader) (plainMessage *VerifyDataReader, err error) { + var entries openpgp.EntityList + checkPacketSequence := !dh.DisableStrictMessageParsing + config := dh.profile.EncryptionConfig() + config.CacheSessionKey = dh.RetrieveSessionKey + config.CheckPacketSequence = &checkPacketSequence + if dh.DecryptionKeyRing != nil { + entries = dh.DecryptionKeyRing.entities + checkIntendedRecipients := !dh.DisableIntendedRecipients + config.CheckIntendedRecipients = &checkIntendedRecipients + } + if dh.VerifyKeyRing != nil { + entries = append(entries, dh.VerifyKeyRing.entities...) + } + verifyTime := dh.clock().Unix() + + config.Time = NewConstantClock(verifyTime) + if dh.VerificationContext != nil { + config.KnownNotations = map[string]bool{constants.SignatureContextName: true} + } + + var messageDetails *openpgp.MessageDetails + if dh.DecryptionKeyRing != nil { + // Private key based decryption + messageDetails, err = openpgp.ReadMessage(encryptedMessage, entries, nil, config) + if err != nil { + return nil, errors.Wrap(err, "gopenpgp: decrypting message with private keys failed") + } + } else { + // Password based decryption + var foundPassword = false + resetReader := internal.NewResetReader(encryptedMessage) + for _, password := range dh.Passwords { + prompt := createPasswordPrompt(password) + messageDetails, err = openpgp.ReadMessage(resetReader, entries, prompt, config) + if err == nil { + foundPassword = true + resetReader.DisableBuffering() + break + } + if _, err := resetReader.Reset(); err != nil { + // Should not happen. + return nil, errors.Wrap(err, "gopenpgp: buffer reset failed") + } + } + if !foundPassword { + // Parsing errors when reading the message are most likely caused by incorrect password, but we cannot know for sure + return nil, errors.New("gopenpgp: error in reading password protected message: wrong password or malformed message") + } + } + + // Add utf8 sanitizer if signature has type packet.SigTypeText + internalReader := messageDetails.UnverifiedBody + if messageDetails.IsSigned && + !dh.DisableAutomaticTextSanitize && + len(messageDetails.SignatureCandidates) > 0 && + messageDetails.SignatureCandidates[len(messageDetails.SignatureCandidates)-1].SigType == packet.SigTypeText { + // TODO: This currently assumes that only one type of signature + // can be present. + internalReader = internal.NewSanitizeReader(internalReader) + } + return &VerifyDataReader{ + messageDetails, + internalReader, + dh.VerifyKeyRing, + config.Time().Unix(), + dh.DisableVerifyTimeCheck, + false, + dh.VerificationContext, + }, nil +} + +func (dh *decryptionHandle) decryptStreamWithSession(dataPacketReader Reader) (plainMessage *VerifyDataReader, err error) { + messageDetails, verifyTime, err := dh.decryptStreamWithSessionAndParse(dataPacketReader) + if err != nil { + return nil, errors.Wrap(err, "gopenpgp: error in reading message") + } + + // Add utf8 sanitizer if signature has type packet.SigTypeText + internalReader := messageDetails.UnverifiedBody + if messageDetails.IsSigned && + !dh.DisableAutomaticTextSanitize && + len(messageDetails.SignatureCandidates) > 0 && + messageDetails.SignatureCandidates[len(messageDetails.SignatureCandidates)-1].SigType == packet.SigTypeText { + // TODO: This currently assumes that only one type of signature + // can be present. + internalReader = internal.NewSanitizeReader(internalReader) + } + return &VerifyDataReader{ + messageDetails, + internalReader, + dh.VerifyKeyRing, + verifyTime, + dh.DisableVerifyTimeCheck, + false, + dh.VerificationContext, + }, err +} + +func (dh *decryptionHandle) decryptStreamWithSessionAndParse(messageReader io.Reader) (*openpgp.MessageDetails, int64, error) { + var keyring openpgp.EntityList + var decrypted io.ReadCloser + var selectedSessionKey *SessionKey + var err error + // Read symmetrically encrypted data packet + for _, sessionKeyCandidate := range dh.SessionKeys { + decrypted, err = decryptStreamWithSessionKey(sessionKeyCandidate, messageReader) + if err == nil { // No error occurred + selectedSessionKey = sessionKeyCandidate + break + } + } + if selectedSessionKey == nil { + return nil, 0, errors.Wrap(err, "gopenpgp: unable to decrypt message with session key") + } + + config := dh.profile.EncryptionConfig() + config.Time = NewConstantClock(dh.clock().Unix()) + + if dh.VerificationContext != nil { + config.KnownNotations = map[string]bool{constants.SignatureContextName: true} + } + + // Push decrypted packet as literal packet and use openpgp's reader + if dh.VerifyKeyRing != nil { + keyring = append(keyring, dh.VerifyKeyRing.entities...) + } + if dh.DecryptionKeyRing != nil { + keyring = append(keyring, dh.DecryptionKeyRing.entities...) + } + checkIntendedRecipients := !dh.DisableIntendedRecipients + checkPacketSequence := false + config.CheckIntendedRecipients = &checkIntendedRecipients + config.CheckPacketSequence = &checkPacketSequence + md, err := openpgp.ReadMessage(decrypted, keyring, nil, config) + if err != nil { + return nil, 0, errors.Wrap(err, "gopenpgp: unable to decode symmetric packet") + } + md.SessionKey = selectedSessionKey.Key + md.UnverifiedBody = checkReader{decrypted, md.UnverifiedBody} + return md, config.Time().Unix(), nil +} + +func decryptStreamWithSessionKey(sessionKey *SessionKey, messageReader io.Reader) (io.ReadCloser, error) { + var decrypted io.ReadCloser + // Read symmetrically encrypted data packet +Loop: + for { + packets := packet.NewReader(messageReader) + p, err := packets.Next() + if err != nil { + return nil, errors.Wrap(err, "gopenpgp: unable to read symmetric packet") + } + + // Decrypt data packet + switch p := p.(type) { + case *packet.EncryptedKey, *packet.SymmetricKeyEncrypted: + // Ignore potential key packets + continue + case *packet.SymmetricallyEncrypted, *packet.AEADEncrypted: + if symPacket, ok := p.(*packet.SymmetricallyEncrypted); ok { + if !symPacket.IntegrityProtected { + return nil, errors.New("gopenpgp: message is not authenticated") + } + } + var dc packet.CipherFunction + if !sessionKey.v6 { + dc, err = sessionKey.GetCipherFunc() + if err != nil { + return nil, errors.Wrap(err, "gopenpgp: unable to decrypt with session key") + } + } + encryptedDataPacket, isDataPacket := p.(packet.EncryptedDataPacket) + if !isDataPacket { + return nil, errors.Wrap(err, "gopenpgp: unknown data packet") + } + decrypted, err = encryptedDataPacket.Decrypt(dc, sessionKey.Key) + if err != nil { + return nil, errors.Wrap(err, "gopenpgp: unable to decrypt symmetric packet") + } + break Loop + default: + return nil, errors.New("gopenpgp: invalid packet type") + } + } + return decrypted, nil +} + +func (dh *decryptionHandle) decryptStreamAndVerifyDetached(encryptedData, encryptedSignature Reader, isPlaintextSignature bool) (plainMessage *VerifyDataReader, err error) { + verifyTime := dh.clock().Unix() + var mdData *openpgp.MessageDetails + signature := encryptedSignature + // Decrypt both messages + if len(dh.SessionKeys) > 0 { + // Decrypt with session key. + mdData, _, err = dh.decryptStreamWithSessionAndParse(encryptedData) + if err != nil { + return nil, errors.Wrap(err, "gopenpgp: error in reading data message") + } + if !isPlaintextSignature { + // Decrypting reader for the encrypted signature + mdSig, _, err := dh.decryptStreamWithSessionAndParse(encryptedSignature) + if err != nil { + return nil, errors.Wrap(err, "gopenpgp: error in reading detached signature message") + } + signature = mdSig.UnverifiedBody + } + } else { + // Password or private keys + checkPacketSequence := !dh.DisableStrictMessageParsing + config := dh.profile.EncryptionConfig() + config.CacheSessionKey = dh.RetrieveSessionKey + config.Time = NewConstantClock(verifyTime) + config.CheckPacketSequence = &checkPacketSequence + var entries openpgp.EntityList + if dh.DecryptionKeyRing != nil { + entries = append(entries, dh.DecryptionKeyRing.entities...) + } + // Decrypting reader for the encrypted data + var selectedPassword []byte + if len(dh.Passwords) > 0 { + resetReader := internal.NewResetReader(encryptedData) + for _, passwordCandidate := range dh.Passwords { + prompt := createPasswordPrompt(passwordCandidate) + mdData, err = openpgp.ReadMessage(resetReader, entries, prompt, config) + if err == nil { // No error occurred + selectedPassword = passwordCandidate + resetReader.DisableBuffering() + break + } + if _, err := resetReader.Reset(); err != nil { + // Should not happen + return nil, errors.Wrap(err, "gopenpgp: buffer reset failed") + } + } + if selectedPassword == nil { + return nil, errors.Wrap(err, "gopenpgp: error in reading data message: no password matched") + } + } else { + mdData, err = openpgp.ReadMessage(encryptedData, entries, nil, config) + if err != nil { + return nil, errors.Wrap(err, "gopenpgp: error in reading data message") + } + } + + if !isPlaintextSignature { + // Decrypting reader for the encrypted signature + prompt := createPasswordPrompt(selectedPassword) + noCheckPacketSequence := false + config.CheckPacketSequence = &noCheckPacketSequence + mdSig, err := openpgp.ReadMessage(encryptedSignature, entries, prompt, config) + if err != nil { + return nil, errors.Wrap(err, "gopenpgp: error in reading detached signature message") + } + signature = mdSig.UnverifiedBody + } + } + + checkIntendedRecipients := !dh.DisableIntendedRecipients + config := dh.profile.EncryptionConfig() + config.CheckIntendedRecipients = &checkIntendedRecipients + + // Verifying reader that wraps the decryption readers to verify the signature + sigVerifyReader, err := verifyingDetachedReader( + mdData.UnverifiedBody, + signature, + dh.VerifyKeyRing, + dh.VerificationContext, + dh.DisableVerifyTimeCheck, + dh.DisableAutomaticTextSanitize, + config, + NewConstantClock(verifyTime), + ) + if err != nil { + return nil, err + } + // Update message details with information from the data of the pgp message + sigVerifyReader.details.LiteralData = mdData.LiteralData + sigVerifyReader.details.SessionKey = mdData.SessionKey + return sigVerifyReader, nil +} + +func getSignaturePacket(sig []byte) (*packet.Signature, error) { + p, err := packet.Read(bytes.NewReader(sig)) + if err != nil { + return nil, err + } + sigPacket, ok := p.(*packet.Signature) + if !ok { + return nil, errors.Wrap(err, "gopenpgp: invalid signature packet") + } + return sigPacket, nil +} + +func createPasswordPrompt(password []byte) func(keys []openpgp.Key, symmetric bool) ([]byte, error) { + if password == nil { + return nil + } + firstTimeCalled := true + return func(keys []openpgp.Key, symmetric bool) ([]byte, error) { + if firstTimeCalled { + firstTimeCalled = false + return password, nil + } + // Re-prompt still occurs if SKESK pasrsing fails (i.e. when decrypted cipher algo is invalid). + // For most (but not all) cases, inputting a wrong passwords is expected to trigger this error. + return nil, errors.New("gopenpgp: wrong password in symmetric decryption") + } +} diff --git a/crypto/decryption_handle.go b/crypto/decryption_handle.go new file mode 100644 index 00000000..c765d183 --- /dev/null +++ b/crypto/decryption_handle.go @@ -0,0 +1,244 @@ +package crypto + +import ( + "bytes" + + "github.com/ProtonMail/go-crypto/openpgp/armor" + "github.com/ProtonMail/gopenpgp/v3/internal" + + "github.com/pkg/errors" +) + +// decryptionHandle collects the configuration parameters to decrypt a pgp message. +// The fields in the struct allow to customize the decryption. +type decryptionHandle struct { + // DecryptionKeyRing provides the secret keys for decrypting the pgp message. + // Assumes that the message was encrypted towards a public key in DecryptionKeyRing. + // If nil, set another field for the type of decryption: SessionKey or Password + DecryptionKeyRing *KeyRing + // SessionKeys provides one or more session keys for decrypting the pgp message. + // Assumes that the message was encrypted with one of the session keys provided. + // If nil, set another field for the type of decryption: DecryptionKeyRing or Password + SessionKeys []*SessionKey + // Passwords provides passwords for decrypting the pgp message. + // Assumes that the message was encrypted with on of the keys derived from the passwords. + // If nil, set another field for the type of decryption: DecryptionKeyRing or SessionKey + Passwords [][]byte + // VerifyKeyRing provides a set of public keys to verify the signature of the pgp message, if any. + // If nil, the signatures are not verified. + VerifyKeyRing *KeyRing + // VerificationContext provides a verification context for the signature of the pgp message, if any. + // Only considered if VerifyKeyRing is not nil. + VerificationContext *VerificationContext + // PlainDetachedSignature indicates that all provided detached signatures are not encrypted. + PlainDetachedSignature bool + // DisableIntendedRecipients indicates if the signature verification should not check if + // the decryption key matches the intended recipients of the message. + // If disabled, the decryption throws no error in a non-matching case. + DisableIntendedRecipients bool + DisableVerifyTimeCheck bool + DisableStrictMessageParsing bool + DisableAutomaticTextSanitize bool + RetrieveSessionKey bool + IsUTF8 bool + clock Clock + profile EncryptionProfile +} + +// --- Default decryption handle to build from + +func defaultDecryptionHandle(profile EncryptionProfile, clock Clock) *decryptionHandle { + return &decryptionHandle{ + clock: clock, + profile: profile, + } +} + +// --- Implements PGPDecryption interface + +// DecryptingReader returns a wrapper around underlying encryptedMessage Reader, +// such that any read-operation via the wrapper results in a read from the decrypted pgp message. +// The returned VerifyDataReader has to be fully read before any potential signatures can be verified. +// Either read the message fully end then call VerifySignature or use the helper method ReadAllAndVerifySignature. +// The encoding indicates if the input message should be unarmored or not, i.e., Bytes/Armor/Auto +// where Auto tries to detect automatically. +// If encryptedMessage is of type PGPSplitReader, the method tries to verify an encrypted detached signature +// that is read from the separate reader. +func (dh *decryptionHandle) DecryptingReader(encryptedMessage Reader, encoding int8) (plainMessageReader *VerifyDataReader, err error) { + err = dh.validate() + if err != nil { + return + } + pgpSplitReader := isPGPSplitReader(encryptedMessage) + if pgpSplitReader != nil { + return dh.decryptingReader(pgpSplitReader, pgpSplitReader.Signature(), encoding) + } + return dh.decryptingReader(encryptedMessage, nil, encoding) +} + +// Decrypt decrypts an encrypted pgp message. +// Returns a VerifiedDataResult, which can be queried for potential signature verification errors, +// and the plaintext data. Note that on a signature error, the method does not return an error. +// Instead, the signature error is stored within the VerifiedDataResult. +// The encoding indicates if the input message should be unarmored or not, i.e., Bytes/Armor/Auto +// where Auto tries to detect automatically. +func (dh *decryptionHandle) Decrypt(pgpMessage []byte, encoding int8) (*VerifiedDataResult, error) { + messageReader := bytes.NewReader(pgpMessage) + plainMessageReader, err := dh.DecryptingReader(messageReader, encoding) + if err != nil { + return nil, err + } + return plainMessageReader.ReadAllAndVerifySignature() +} + +// DecryptDetached provides the same functionality as Decrypt but allows +// to supply an encrypted detached signature that should be decrypted and verified +// against the data in the pgp message. If encDetachedSignature is nil, the behavior is similar +// to Decrypt. The encoding indicates if the input message should be unarmored or not, +// i.e., Bytes/Armor/Auto where Auto tries to detect automatically. +func (dh *decryptionHandle) DecryptDetached(pgpMessage []byte, encryptedDetachedSig []byte, encoding int8) (*VerifiedDataResult, error) { + reader := &pgpSplitReader{ + encMessage: bytes.NewReader(pgpMessage), + } + if encryptedDetachedSig != nil { + reader.encSignature = bytes.NewReader(encryptedDetachedSig) + } + verifier, err := dh.DecryptingReader(reader, encoding) + if err != nil { + return nil, err + } + return verifier.ReadAllAndVerifySignature() +} + +// DecryptSessionKey decrypts an encrypted session key. +// To decrypted a session key, the decryption handle must contain either a decryption key or a password. +func (dh *decryptionHandle) DecryptSessionKey(keyPackets []byte) (sk *SessionKey, err error) { + switch { + case len(dh.Passwords) > 0: + for _, passwordCandidate := range dh.Passwords { + sk, err = decryptSessionKeyWithPassword(keyPackets, passwordCandidate) + if err == nil { + return sk, nil + } + } + return nil, err + case dh.DecryptionKeyRing != nil: + return decryptSessionKey(dh.DecryptionKeyRing, keyPackets) + } + return nil, errors.New("gopenpgp: no decryption key or password provided") +} + +// ClearPrivateParams clears all private key material contained in EncryptionHandle from memory. +func (dh *decryptionHandle) ClearPrivateParams() { + if dh.DecryptionKeyRing != nil { + dh.DecryptionKeyRing.ClearPrivateParams() + } + if len(dh.SessionKeys) > 0 { + for _, sk := range dh.SessionKeys { + sk.Clear() + } + } + if len(dh.Passwords) > 0 { + for _, password := range dh.Passwords { + clearMem(password) + } + } +} + +func (dh *decryptionHandle) validate() error { + if dh.DecryptionKeyRing == nil && len(dh.Passwords) == 0 && len(dh.SessionKeys) == 0 { + return errors.New("gopenpgp: no decryption key material provided") + } + return nil +} + +func (dh *decryptionHandle) decryptingReader(encryptedMessage Reader, encryptedSignature Reader, encoding int8) (plainMessageReader *VerifyDataReader, err error) { + err = dh.validate() + if err != nil { + return nil, err + } + var armored bool + encryptedMessage, armored = unarmorInput(encoding, encryptedMessage) + var armoredBlock *armor.Block + if armored { + // Wrap encryptedMessage with decode armor reader. + armoredBlock, err = armor.Decode(encryptedMessage) + if err != nil { + err = errors.Wrap(err, "gopenpgp: unarmor failed for pgp message") + return nil, err + } + encryptedMessage = armoredBlock.Body + } + if encryptedSignature != nil { + encryptedSignature, armored = unarmorInput(encoding, encryptedSignature) + if armored { + // Wrap encryptedSignature with decode armor reader. + armoredBlock, err = armor.Decode(encryptedSignature) + if err != nil { + err = errors.Wrap(err, "gopenpgp: unarmor failed for pgp encrypted signature message") + return nil, err + } + encryptedSignature = armoredBlock.Body + if dh.PlainDetachedSignature && armoredBlock.Type != "PGP SIGNATURE" { + err = errors.New("gopenpgp: detached signature is not plaintext") + return nil, err + } + if !dh.PlainDetachedSignature && armoredBlock.Type != "PGP MESSAGE" { + err = errors.New("gopenpgp: encrypted detached signature is not an encrypted pgp message") + return nil, err + } + } + } + + var decryptionTried bool + if len(dh.SessionKeys) > 0 { + // Decrypt with session key. + if encryptedSignature != nil { + plainMessageReader, err = dh.decryptStreamAndVerifyDetached(encryptedMessage, encryptedSignature, dh.PlainDetachedSignature) + } else { + plainMessageReader, err = dh.decryptStreamWithSession(encryptedMessage) + } + decryptionTried = true + } + if (!decryptionTried || err != nil) && dh.DecryptionKeyRing != nil { + // Decrypt with keyring. + if encryptedSignature != nil { + plainMessageReader, err = dh.decryptStreamAndVerifyDetached(encryptedMessage, encryptedSignature, dh.PlainDetachedSignature) + } else { + plainMessageReader, err = dh.decryptStream(encryptedMessage) + } + decryptionTried = true + } + if (!decryptionTried || err != nil) && len(dh.Passwords) > 0 { + // Decrypt with password. + if encryptedSignature != nil { + plainMessageReader, err = dh.decryptStreamAndVerifyDetached(encryptedMessage, encryptedSignature, dh.PlainDetachedSignature) + } else { + plainMessageReader, err = dh.decryptStream(encryptedMessage) + } + decryptionTried = true + } + if !decryptionTried { + // No decryption material provided. + err = errors.New("gopenpgp: no decryption key ring, session key, or password provided") + } + if err != nil { + return nil, err + } + if dh.IsUTF8 { + plainMessageReader.internalReader = internal.NewSanitizeReader(plainMessageReader.internalReader) + } + return plainMessageReader, nil +} + +func isPGPSplitReader(w Reader) PGPSplitReader { + v, ok := interface{}(w).(PGPSplitReader) + if ok { + return v + } + v, ok = interface{}(&w).(PGPSplitReader) + if ok { + return v + } + return nil +} diff --git a/crypto/decryption_handle_builder.go b/crypto/decryption_handle_builder.go new file mode 100644 index 00000000..6876a74b --- /dev/null +++ b/crypto/decryption_handle_builder.go @@ -0,0 +1,182 @@ +package crypto + +// DecryptionHandleBuilder allows to configure a decryption handle +// to decrypt a pgp message. +type DecryptionHandleBuilder struct { + handle *decryptionHandle + defaultClock Clock + err error + profile EncryptionProfile +} + +func newDecryptionHandleBuilder(profile EncryptionProfile, clock Clock) *DecryptionHandleBuilder { + return &DecryptionHandleBuilder{ + handle: defaultDecryptionHandle(profile, clock), + defaultClock: clock, + profile: profile, + } +} + +// DecryptionKeys sets the secret keys for decrypting the pgp message. +// Assumes that the message was encrypted towards one of the secret keys. +// Triggers the hybrid decryption mode. +// If not set, set another field for the type of decryption: SessionKey or Password. +func (dpb *DecryptionHandleBuilder) DecryptionKeys(decryptionKeyRing *KeyRing) *DecryptionHandleBuilder { + dpb.handle.DecryptionKeyRing = decryptionKeyRing + return dpb +} + +func (dpb *DecryptionHandleBuilder) DecryptionKey(decryptionKey *Key) *DecryptionHandleBuilder { + var err error + if dpb.handle.DecryptionKeyRing == nil { + dpb.handle.DecryptionKeyRing, err = NewKeyRing(decryptionKey) + } else { + err = dpb.handle.DecryptionKeyRing.AddKey(decryptionKey) + } + dpb.err = err + return dpb +} + +// SessionKey sets a session key for decrypting the pgp message. +// Assumes that the message was encrypted with session key provided. +// Triggers the session key decryption mode. +// If not set, set another field for the type of decryption: DecryptionKeys or Password. +func (dpb *DecryptionHandleBuilder) SessionKey(sessionKey *SessionKey) *DecryptionHandleBuilder { + dpb.handle.SessionKeys = []*SessionKey{sessionKey} + return dpb +} + +// SessionKeys sets multiple session keys for decrypting the pgp message. +// Assumes that the message was encrypted with one of the session keys provided. +// Triggers the session key decryption mode. +// If not set, set another field for the type of decryption: DecryptionKeys or Password. +// Not supported on go-mobile clients. +func (dpb *DecryptionHandleBuilder) SessionKeys(sessionKeys []*SessionKey) *DecryptionHandleBuilder { + dpb.handle.SessionKeys = sessionKeys + return dpb +} + +// Password sets a password that is used to derive a key to decrypt the pgp message. +// Assumes that the message was encrypted with a key derived from the password. +// Triggers the password decryption mode. +// If not set, set another field for the type of decryption: DecryptionKeys or SessionKey. +func (dpb *DecryptionHandleBuilder) Password(password []byte) *DecryptionHandleBuilder { + dpb.handle.Passwords = [][]byte{password} + return dpb +} + +// Passwords sets passwords that are used to derive keys to decrypt the pgp message. +// Assumes that the message was encrypted with one of the keys derived from the passwords. +// Triggers the password decryption mode. +// If not set, set another field for the type of decryption: DecryptionKeys or SessionKey. +// Not supported on go-mobile clients. +func (dpb *DecryptionHandleBuilder) Passwords(passwords [][]byte) *DecryptionHandleBuilder { + dpb.handle.Passwords = passwords + return dpb +} + +// VerificationKeys sets the public keys for verifying the signatures of the pgp message, if any. +// If not set, the signatures cannot be verified. +func (dpb *DecryptionHandleBuilder) VerificationKeys(keys *KeyRing) *DecryptionHandleBuilder { + dpb.handle.VerifyKeyRing = keys + return dpb +} + +// VerificationKey sets the public key for verifying the signatures of the pgp message, if any. +// If not set, the signatures cannot be verified. +func (dpb *DecryptionHandleBuilder) VerificationKey(key *Key) *DecryptionHandleBuilder { + var err error + if dpb.handle.VerifyKeyRing == nil { + dpb.handle.VerifyKeyRing, err = NewKeyRing(key) + } else { + err = dpb.handle.VerifyKeyRing.AddKey(key) + } + dpb.err = err + return dpb +} + +// VerificationContext sets a verification context for signatures of the pgp message, if any. +// Only considered if VerifyKeys are set. +func (dpb *DecryptionHandleBuilder) VerificationContext(verifyContext *VerificationContext) *DecryptionHandleBuilder { + dpb.handle.VerificationContext = verifyContext + return dpb +} + +// VerifyTime sets the verification time to the provided timestamp. +// If not set, the systems current time is used for signature verification. +func (dpb *DecryptionHandleBuilder) VerifyTime(unixTime int64) *DecryptionHandleBuilder { + dpb.handle.clock = NewConstantClock(unixTime) + return dpb +} + +// Utf8 indicates if the output plaintext is Utf8 and +// should be sanitized from canonicalised line endings. +func (dpb *DecryptionHandleBuilder) Utf8() *DecryptionHandleBuilder { + dpb.handle.IsUTF8 = true + return dpb +} + +// PlainDetachedSignature indicates that the detached signature to verify is not decrypted +// and can be verified as is. +func (dpb *DecryptionHandleBuilder) PlainDetachedSignature() *DecryptionHandleBuilder { + dpb.handle.PlainDetachedSignature = true + return dpb +} + +// DisableVerifyTimeCheck disables the check for comparing the signature creation time +// against the verification time. +func (dpb *DecryptionHandleBuilder) DisableVerifyTimeCheck() *DecryptionHandleBuilder { + dpb.handle.DisableVerifyTimeCheck = true + return dpb +} + +// DisableStrictMessageParsing disables the check that decryption inputs conform +// to the OpenPGP Message grammar. +// If set, the decryption methods return no error if the message does not conform to the +// OpenPGP message grammar. +func (dpb *DecryptionHandleBuilder) DisableStrictMessageParsing() *DecryptionHandleBuilder { + dpb.handle.DisableStrictMessageParsing = true + return dpb +} + +// DisableIntendedRecipients indicates if the signature verification should not check if +// the decryption key matches the intended recipients of the message. +// If disabled, the decryption methods throw no error in a non-matching case. +func (dpb *DecryptionHandleBuilder) DisableIntendedRecipients() *DecryptionHandleBuilder { + dpb.handle.DisableIntendedRecipients = true + return dpb +} + +// DisableAutomaticTextSanitize indicates that automatic text sanitization should be disabled. +// If not disabled, the output will be sanitized if a text signature is present. +func (dpb *DecryptionHandleBuilder) DisableAutomaticTextSanitize() *DecryptionHandleBuilder { + dpb.handle.DisableAutomaticTextSanitize = true + return dpb +} + +// RetrieveSessionKey sets the flag to indicate if the session key used for decryption +// should be returned to the caller of the decryption function. +func (dpb *DecryptionHandleBuilder) RetrieveSessionKey() *DecryptionHandleBuilder { + dpb.handle.RetrieveSessionKey = true + return dpb +} + +// New creates a DecryptionHandle and checks that the given +// combination of parameters is valid. If one of the parameters are invalid +// the latest error is returned. +func (dpb *DecryptionHandleBuilder) New() (PGPDecryption, error) { + if dpb.err != nil { + return nil, dpb.err + } + dpb.err = dpb.handle.validate() + if dpb.err != nil { + return nil, dpb.err + } + handle := dpb.handle + dpb.handle = defaultDecryptionHandle(dpb.profile, dpb.defaultClock) + return handle, nil +} + +func (dpb *DecryptionHandleBuilder) Error() error { + return dpb.err +} diff --git a/crypto/encoding.go b/crypto/encoding.go new file mode 100644 index 00000000..20263486 --- /dev/null +++ b/crypto/encoding.go @@ -0,0 +1,32 @@ +package crypto + +import ( + "io" + + armorHelper "github.com/ProtonMail/gopenpgp/v3/armor" +) + +type PGPEncoding int8 + +// PGPEncoding determines the message encoding. +// The type is int8 for compatibility with gomobile. +const ( + Armor int8 = 0 + Bytes int8 = 1 // Default for other int8 values. + Auto int8 = 2 +) + +func armorOutput(e int8) bool { + return e == Armor +} + +func unarmorInput(e int8, input io.Reader) (reader Reader, unarmor bool) { + reader = input + switch e { + case Armor: + unarmor = true + case Auto: + reader, unarmor = armorHelper.IsPGPArmored(input) + } + return +} diff --git a/crypto/encrypt_decrypt_err_test.go b/crypto/encrypt_decrypt_err_test.go new file mode 100644 index 00000000..4b3a64ca --- /dev/null +++ b/crypto/encrypt_decrypt_err_test.go @@ -0,0 +1,237 @@ +package crypto + +import ( + "testing" +) + +const wrongTestContext = "wrong-context" + +var wrongPassword = []byte("wrong-password") + +func TestDecryptWrongKey(t *testing.T) { + for _, material := range testMaterialForProfiles { + t.Run(material.profileName, func(t *testing.T) { + encHandle, _ := material.pgp.Encryption(). + Recipients(material.keyRingTestPublic). + SigningKeys(material.keyRingTestPrivate). + New() + decHandle, _ := material.pgp.Decryption(). + DecryptionKey(material.keyWrong). + New() + + pgpMessage, err := encHandle.Encrypt([]byte(testMessage)) + if err != nil { + t.Fatal(err) + } + if _, err = decHandle.Decrypt(pgpMessage.Bytes(), Bytes); err == nil { + t.Fatal("should not decrypt with wrong key") + } + }) + } +} + +func TestDecryptWrongPassword(t *testing.T) { + for _, material := range testMaterialForProfiles { + t.Run(material.profileName, func(t *testing.T) { + encHandle, _ := material.pgp.Encryption(). + Password(password). + New() + decHandle, _ := material.pgp.Decryption(). + Password(wrongPassword). + New() + + pgpMessage, err := encHandle.Encrypt([]byte(testMessage)) + if err != nil { + t.Fatal(err) + } + if _, err = decHandle.Decrypt(pgpMessage.Bytes(), Bytes); err == nil { + t.Fatal("should not decrypt with wrong password") + } + }) + } +} + +func TestDecryptWrongSessionKey(t *testing.T) { + for _, material := range testMaterialForProfiles { + t.Run(material.profileName, func(t *testing.T) { + wrongSessionKeyBytes := make([]byte, len(material.testSessionKey.Key)) + copy(wrongSessionKeyBytes, material.testSessionKey.Key) + wrongSessionKeyBytes[0] += 1 + wrongSessionKey := NewSessionKeyFromToken(wrongSessionKeyBytes, material.testSessionKey.Algo) + + encHandle, _ := material.pgp.Encryption(). + SessionKey(material.testSessionKey). + New() + decHandle, _ := material.pgp.Decryption(). + SessionKey(wrongSessionKey). + New() + + pgpMessage, err := encHandle.Encrypt([]byte(testMessage)) + if err != nil { + t.Fatal(err) + } + if _, err = decHandle.Decrypt(pgpMessage.Bytes(), Bytes); err == nil { + t.Fatal("Should not decrypt with wrong session key") + } + }) + } +} + +func TestDecryptVerifyWrongKey(t *testing.T) { + for _, material := range testMaterialForProfiles { + t.Run(material.profileName, func(t *testing.T) { + encHandle, _ := material.pgp.Encryption(). + Recipients(material.keyRingTestPublic). + SigningKey(material.keyWrong). + New() + decHandle, _ := material.pgp.Decryption(). + DecryptionKeys(material.keyRingTestPrivate). + VerificationKeys(material.keyRingTestPublic). + New() + testVerificationFails( + t, + encHandle, + decHandle, + "Signature verification must fail with wrong key", + ) + }) + } +} + +func TestDecryptVerifyWrongContext(t *testing.T) { + for _, material := range testMaterialForProfiles { + t.Run(material.profileName, func(t *testing.T) { + encHandle, _ := material.pgp.Encryption(). + Recipients(material.keyRingTestPublic). + SigningKeys(material.keyRingTestPrivate). + SigningContext(NewSigningContext(testContext, true)). + New() + decHandle, _ := material.pgp.Decryption(). + DecryptionKeys(material.keyRingTestPrivate). + VerificationKeys(material.keyRingTestPublic). + VerificationContext(NewVerificationContext(wrongTestContext, true, 0)). + New() + testVerificationFails( + t, + encHandle, + decHandle, + "Signature verification must fail with wrong context", + ) + }) + } +} + +func TestDecryptVerifyWrongContextMissing(t *testing.T) { + for _, material := range testMaterialForProfiles { + t.Run(material.profileName, func(t *testing.T) { + encHandle, _ := material.pgp.Encryption(). + Recipients(material.keyRingTestPublic). + SigningKeys(material.keyRingTestPrivate). + SigningContext(NewSigningContext(testContext, true)). + New() + decHandle, _ := material.pgp.Decryption(). + DecryptionKeys(material.keyRingTestPrivate). + VerificationKeys(material.keyRingTestPublic). + New() + testVerificationFails( + t, + encHandle, + decHandle, + "Signature verification must fail with no context", + ) + }) + } +} + +func TestDecryptVerifyNoContextButRequired(t *testing.T) { + for _, material := range testMaterialForProfiles { + t.Run(material.profileName, func(t *testing.T) { + encHandle, _ := material.pgp.Encryption(). + Recipients(material.keyRingTestPublic). + SigningKeys(material.keyRingTestPrivate). + New() + decHandle, _ := material.pgp.Decryption(). + DecryptionKeys(material.keyRingTestPrivate). + VerificationKeys(material.keyRingTestPublic). + VerificationContext(NewVerificationContext(wrongTestContext, true, 0)). + New() + testVerificationFails( + t, + encHandle, + decHandle, + "Signature verification must fail with no context", + ) + }) + } +} + +func TestDecryptVerifyNoCriticalContext(t *testing.T) { + for _, material := range testMaterialForProfiles { + t.Run(material.profileName, func(t *testing.T) { + encHandle, _ := material.pgp.Encryption(). + Recipients(material.keyRingTestPublic). + SigningKeys(material.keyRingTestPrivate). + SigningContext(NewSigningContext(testContext, false)). + New() + decHandle, _ := material.pgp.Decryption(). + DecryptionKeys(material.keyRingTestPrivate). + VerificationKeys(material.keyRingTestPublic). + New() + testVerificationSuccess(t, encHandle, decHandle) + }) + } +} + +func TestDecryptVerifyNoCriticalContextVerify(t *testing.T) { + for _, material := range testMaterialForProfiles { + t.Run(material.profileName, func(t *testing.T) { + encHandle, _ := material.pgp.Encryption(). + Recipients(material.keyRingTestPublic). + SigningKeys(material.keyRingTestPrivate). + New() + decHandle, _ := material.pgp.Decryption(). + DecryptionKeys(material.keyRingTestPrivate). + VerificationKeys(material.keyRingTestPublic). + VerificationContext(NewVerificationContext(testContext, false, 0)). + New() + testVerificationSuccess(t, encHandle, decHandle) + }) + } +} + +func testVerificationFails( + t *testing.T, + encHandle PGPEncryption, + decHandle PGPDecryption, + failure string, +) { + pgpMessage, err := encHandle.Encrypt([]byte(testMessage)) + if err != nil { + t.Fatal(err) + } + verifyResult, err := decHandle.Decrypt(pgpMessage.Bytes(), Bytes) + if err != nil { + t.Fatal(err) + } + if verifyResult.SignatureError() == nil { + t.Fatal(failure) + } +} + +func testVerificationSuccess( + t *testing.T, + encHandle PGPEncryption, + decHandle PGPDecryption, +) { + pgpMessage, err := encHandle.Encrypt([]byte(testMessage)) + if err != nil { + t.Fatal(err) + } + verifyResult, err := decHandle.Decrypt(pgpMessage.Bytes(), Bytes) + if err != nil { + t.Fatal(err) + } + if verifyResult.SignatureError() != nil { + t.Fatal("Expected no signature failure") + } +} diff --git a/crypto/encrypt_decrypt_test.go b/crypto/encrypt_decrypt_test.go new file mode 100644 index 00000000..5eae4c6c --- /dev/null +++ b/crypto/encrypt_decrypt_test.go @@ -0,0 +1,1242 @@ +package crypto + +import ( + "bytes" + "fmt" + "io" + "os" + "reflect" + "regexp" + "strings" + "testing" + + openpgp "github.com/ProtonMail/go-crypto/openpgp/v2" + "github.com/ProtonMail/gopenpgp/v3/profile" + "github.com/stretchr/testify/assert" +) + +const testMessageString = "Hello World!" +const testMessageUTF8 = "Hell\ro World!\nmore\neven more\n" +const testContext = "test-context" + +var password = []byte("password") +var decPasswords = [][]byte{[]byte("wrongPassword"), password} +var testMaterialForProfiles []*testMaterial + +func generateTestKeyMaterial(profile *profile.Custom) *testMaterial { + handle := PGPWithProfile(profile) + testSessionKey, err := handle.GenerateSessionKey() + if err != nil { + panic("Cannot generate session key:" + err.Error()) + } + + keyTest, err := handle.KeyGeneration(). + AddUserId("test", "test@test.test"). + New(). + GenerateKey() + if err != nil { + panic("Cannot generate key:" + err.Error()) + } + keyTestPublic, err := keyTest.ToPublic() + if err != nil { + panic("Cannot extract public key:" + err.Error()) + } + keyRingTestPrivate, err := NewKeyRing(keyTest) + if err != nil { + panic("Cannot create keyring:" + err.Error()) + } + keyRingTestPublic, err := NewKeyRing(keyTestPublic) + if err != nil { + panic("Cannot create keyring:" + err.Error()) + } + keyWrong, err := handle.KeyGeneration(). + AddUserId("testWrong", "testWrong@test.test"). + New(). + GenerateKey() + if err != nil { + panic("Cannot generate key:" + err.Error()) + } + return &testMaterial{ + profileName: profile.Name, + pgp: handle, + keyRingTestPublic: keyRingTestPublic, + keyRingTestPrivate: keyRingTestPrivate, + testSessionKey: testSessionKey, + keyWrong: keyWrong, + } +} + +func initEncDecTest() { + for _, profile := range testProfiles { + material := generateTestKeyMaterial(profile) + testMaterialForProfiles = append(testMaterialForProfiles, material) + } + if len(testMaterialForProfiles) < 2 { + return + } + firstMaterial := testMaterialForProfiles[0] + lastMaterial := testMaterialForProfiles[len(testMaterialForProfiles)-1] + // Mixed keys with different profiles + mixKeyringPriv := &KeyRing{ + entities: openpgp.EntityList{ + firstMaterial.keyRingTestPrivate.entities[0], + lastMaterial.keyRingTestPrivate.entities[0], + }, + } + mixKeyringPub := &KeyRing{ + entities: openpgp.EntityList{ + firstMaterial.keyRingTestPublic.entities[0], + lastMaterial.keyRingTestPublic.entities[0], + }, + } + mixedTestMaterial := &testMaterial{ + profileName: fmt.Sprintf("mixed(%s)(%s)", firstMaterial.profileName, lastMaterial.profileName), + pgp: lastMaterial.pgp, + keyRingTestPublic: mixKeyringPub, + keyRingTestPrivate: mixKeyringPriv, + testSessionKey: lastMaterial.testSessionKey, + } + testMaterialForProfiles = append(testMaterialForProfiles, mixedTestMaterial) +} + +type testMaterial struct { + profileName string + pgp *PGPHandle + keyRingTestPublic *KeyRing + keyRingTestPrivate *KeyRing + keyWrong *Key + testSessionKey *SessionKey +} + +func TestEncryptDecryptStream(t *testing.T) { + for _, material := range testMaterialForProfiles { + t.Run(material.profileName, func(t *testing.T) { + encHandle, _ := material.pgp.Encryption(). + Recipients(material.keyRingTestPublic). + SigningKeys(material.keyRingTestPrivate). + New() + decHandle, _ := material.pgp.Decryption(). + DecryptionKeys(material.keyRingTestPrivate). + VerificationKeys(material.keyRingTestPublic). + New() + testEncryptDecryptStream( + t, + []byte(testMessageString), + nil, + encHandle, + decHandle, + len(material.keyRingTestPrivate.entities), + Bytes, + ) + }) + } +} + +func TestEncryptDecryptStreamWithContext(t *testing.T) { + for _, material := range testMaterialForProfiles { + t.Run(material.profileName, func(t *testing.T) { + encHandle, _ := material.pgp.Encryption(). + Recipients(material.keyRingTestPublic). + SigningKeys(material.keyRingTestPrivate). + SigningContext(NewSigningContext(testContext, true)). + New() + decHandle, _ := material.pgp.Decryption(). + DecryptionKeys(material.keyRingTestPrivate). + VerificationKeys(material.keyRingTestPublic). + VerificationContext(NewVerificationContext(testContext, true, 0)). + New() + testEncryptDecryptStream( + t, + []byte(testMessageString), + nil, + encHandle, + decHandle, + len(material.keyRingTestPrivate.entities), + Bytes, + ) + }) + } +} + +func TestEncryptDecryptStreamWithContextAndCompression(t *testing.T) { + for _, material := range testMaterialForProfiles { + t.Run(material.profileName, func(t *testing.T) { + encHandle, _ := material.pgp.Encryption(). + Recipients(material.keyRingTestPublic). + SigningKeys(material.keyRingTestPrivate). + SigningContext(NewSigningContext(testContext, true)). + Compress(). + New() + decHandle, _ := material.pgp.Decryption(). + DecryptionKeys(material.keyRingTestPrivate). + VerificationKeys(material.keyRingTestPublic). + VerificationContext(NewVerificationContext(testContext, true, 0)). + New() + testEncryptDecryptStream( + t, + []byte(testMessageString), + nil, + encHandle, + decHandle, + len(material.keyRingTestPrivate.entities), + Bytes, + ) + }) + } +} + +func TestEncryptDecryptStreamWithCachedSession(t *testing.T) { + for _, material := range testMaterialForProfiles { + t.Run(material.profileName, func(t *testing.T) { + encHandle, _ := material.pgp.Encryption(). + Recipients(material.keyRingTestPublic). + SigningKeys(material.keyRingTestPrivate). + SessionKey(material.testSessionKey). + New() + decHandle, _ := material.pgp.Decryption(). + SessionKey(material.testSessionKey). + VerificationKeys(material.keyRingTestPublic). + DisableIntendedRecipients(). + New() + testEncryptDecryptStream( + t, + []byte(testMessageString), + nil, + encHandle, + decHandle, + len(material.keyRingTestPrivate.entities), + Bytes, + ) + }) + } +} + +func TestEncryptDecryptCachedSessionOnDecrypt(t *testing.T) { + for _, material := range testMaterialForProfiles { + t.Run(material.profileName, func(t *testing.T) { + encHandle, _ := material.pgp.Encryption(). + Recipients(material.keyRingTestPublic). + SigningKeys(material.keyRingTestPrivate). + SessionKey(material.testSessionKey). + New() + decHandle, _ := material.pgp.Decryption(). + DecryptionKeys(material.keyRingTestPrivate). + VerificationKeys(material.keyRingTestPublic). + RetrieveSessionKey(). + New() + pgpMessage, err := encHandle.Encrypt([]byte(testMessageString)) + if err != nil { + t.Fatal("Expected no error in encryption, got:", err) + } + decResult, err := decHandle.Decrypt(pgpMessage.Bytes(), Bytes) + if err != nil { + t.Fatal("Expected no error in decryption, got:", decResult) + } + if !bytes.Equal(decResult.SessionKey().Key, material.testSessionKey.Key) { + t.Fatal("Expected the cached session key to be equal") + } + if decResult.SessionKey().Algo != material.testSessionKey.Algo { + t.Fatal("Expected the session key algorithms to be equal") + } + decHandle, _ = material.pgp.Decryption(). + DecryptionKeys(material.keyRingTestPrivate). + VerificationKeys(material.keyRingTestPublic). + New() + decResult, err = decHandle.Decrypt(pgpMessage.Bytes(), Bytes) + if err != nil { + t.Fatal("Expected no error in decryption, got:", decResult) + } + if decResult.SessionKey() != nil { + t.Fatal("Expected no cached session key") + } + }) + } +} + +func TestSessionEncryptDecryptStream(t *testing.T) { + for _, material := range testMaterialForProfiles { + t.Run(material.profileName, func(t *testing.T) { + encHandle, _ := material.pgp.Encryption(). + SessionKey(material.testSessionKey). + SigningKeys(material.keyRingTestPrivate). + New() + decHandle, _ := material.pgp.Decryption(). + SessionKey(material.testSessionKey). + VerificationKeys(material.keyRingTestPublic). + New() + testEncryptDecryptStream( + t, + []byte(testMessageString), + nil, + encHandle, + decHandle, + len(material.keyRingTestPrivate.entities), + Bytes, + ) + }) + } +} + +func TestSessionEncryptDecryptStreamWithContext(t *testing.T) { + for _, material := range testMaterialForProfiles { + t.Run(material.profileName, func(t *testing.T) { + encHandle, _ := material.pgp.Encryption(). + SessionKey(material.testSessionKey). + SigningKeys(material.keyRingTestPrivate). + SigningContext(NewSigningContext(testContext, true)). + New() + decHandle, _ := material.pgp.Decryption(). + SessionKey(material.testSessionKey). + VerificationKeys(material.keyRingTestPublic). + VerificationContext(NewVerificationContext(testContext, true, 0)). + New() + testEncryptDecryptStream( + t, + []byte(testMessageString), + nil, + encHandle, + decHandle, + len(material.keyRingTestPrivate.entities), + Bytes, + ) + }) + } +} + +func TestSessionEncryptDecryptStreamWithContextAndCompression(t *testing.T) { + for _, material := range testMaterialForProfiles { + t.Run(material.profileName, func(t *testing.T) { + encHandle, _ := material.pgp.Encryption(). + SessionKey(material.testSessionKey). + SigningKeys(material.keyRingTestPrivate). + SigningContext(NewSigningContext(testContext, true)). + Compress(). + New() + decHandle, _ := material.pgp.Decryption(). + SessionKey(material.testSessionKey). + VerificationKeys(material.keyRingTestPublic). + VerificationContext(NewVerificationContext(testContext, true, 0)). + New() + testEncryptDecryptStream( + t, + []byte(testMessageString), + nil, + encHandle, + decHandle, + len(material.keyRingTestPrivate.entities), + Bytes, + ) + }) + } +} + +func TestEncryptDecryptStreamArmored(t *testing.T) { + for _, material := range testMaterialForProfiles { + t.Run(material.profileName, func(t *testing.T) { + encHandle, _ := material.pgp.Encryption(). + Recipients(material.keyRingTestPublic). + SigningKeys(material.keyRingTestPrivate). + New() + decHandle, _ := material.pgp.Decryption(). + DecryptionKeys(material.keyRingTestPrivate). + VerificationKeys(material.keyRingTestPublic). + New() + testEncryptDecryptStream( + t, + []byte(testMessageString), + nil, + encHandle, + decHandle, + len(material.keyRingTestPrivate.entities), + Armor, + ) + }) + } +} + +func TestEncryptDecryptSignUTF8Stream(t *testing.T) { + for _, material := range testMaterialForProfiles { + metadata := &LiteralMetadata{ + isUTF8: true, + } + t.Run(material.profileName, func(t *testing.T) { + encHandle, _ := material.pgp.Encryption(). + Recipients(material.keyRingTestPublic). + SigningKeys(material.keyRingTestPrivate). + Utf8(). + New() + decHandle, _ := material.pgp.Decryption(). + DecryptionKeys(material.keyRingTestPrivate). + VerificationKeys(material.keyRingTestPublic). + New() + testEncryptDecryptStream( + t, + []byte(testMessageUTF8), + metadata, + encHandle, + decHandle, + len(material.keyRingTestPrivate.entities), + Bytes, + ) + }) + } +} + +func TestEncryptDecryptUTF8Stream(t *testing.T) { + for _, material := range testMaterialForProfiles { + metadata := &LiteralMetadata{ + isUTF8: true, + } + t.Run(material.profileName, func(t *testing.T) { + encHandle, _ := material.pgp.Encryption(). + Recipients(material.keyRingTestPublic). + Utf8(). + New() + decHandle, _ := material.pgp.Decryption(). + DecryptionKeys(material.keyRingTestPrivate). + Utf8(). + New() + testEncryptDecryptStream( + t, + []byte(testMessageUTF8), + metadata, + encHandle, + decHandle, + 0, + Bytes, + ) + }) + } +} + +func TestAEADDecryptionStream(t *testing.T) { + pgpMessageDataReader, err := os.Open("testdata/gpg2.3-aead-pgp-message.pgp") + if err != nil { + t.Fatal("Expected no error when reading message data, got:", err) + } + aeadKey, err := NewKeyFromArmored(readTestFile("gpg2.3-aead-test-key.asc", false)) + if err != nil { + t.Fatal("Expected no error when unarmoring key, got:", err) + } + + aeadKeyUnlocked, err := aeadKey.Unlock([]byte("test")) + if err != nil { + t.Fatal("Expected no error when unlocking, got:", err) + } + kR, err := NewKeyRing(aeadKeyUnlocked) + if err != nil { + t.Fatal("Expected no error when creating the keyring, got:", err) + } + defer kR.ClearPrivateParams() + + decHandle, _ := PGP().Decryption().DecryptionKeys(kR).New() + messageReader, err := decHandle.DecryptingReader(pgpMessageDataReader, Auto) + if err != nil { + t.Fatal("Expected no error when decrypting, got:", err) + } + plaintext, err := messageReader.ReadAll() + if err != nil { + t.Fatal("Expected no error when decrypting, got:", err) + } + + assert.Exactly(t, "hello world\n", string(plaintext)) +} + +func TestEncryptDecryptSplitStream(t *testing.T) { + for _, material := range testMaterialForProfiles { + t.Run(material.profileName, func(t *testing.T) { + encHandle, _ := material.pgp.Encryption(). + Recipients(material.keyRingTestPublic). + SigningKeys(material.keyRingTestPrivate). + New() + decHandle, _ := material.pgp.Decryption(). + DecryptionKeys(material.keyRingTestPrivate). + VerificationKeys(material.keyRingTestPublic). + New() + testEncryptSplitDecryptStream( + t, + []byte(testMessageString), + nil, + encHandle, + decHandle, + splitWriter, + len(material.keyRingTestPrivate.entities), + Bytes, + ) + }) + } +} + +func TestEncryptDecryptSplitStreamWithContext(t *testing.T) { + for _, material := range testMaterialForProfiles { + t.Run(material.profileName, func(t *testing.T) { + encHandle, _ := material.pgp.Encryption(). + Recipients(material.keyRingTestPublic). + SigningKeys(material.keyRingTestPrivate). + SigningContext(NewSigningContext(testContext, true)). + New() + decHandle, _ := material.pgp.Decryption(). + DecryptionKeys(material.keyRingTestPrivate). + VerificationKeys(material.keyRingTestPublic). + VerificationContext(NewVerificationContext(testContext, true, 0)). + New() + testEncryptSplitDecryptStream( + t, + []byte(testMessageString), + nil, + encHandle, + decHandle, + splitWriter, + len(material.keyRingTestPrivate.entities), + Bytes, + ) + }) + } +} + +func TestEncryptDecryptSplitStreamWithContextAndCompression(t *testing.T) { + for _, material := range testMaterialForProfiles { + t.Run(material.profileName, func(t *testing.T) { + encHandle, _ := material.pgp.Encryption(). + Recipients(material.keyRingTestPublic). + SigningKeys(material.keyRingTestPrivate). + SigningContext(NewSigningContext(testContext, true)). + Compress(). + New() + decHandle, _ := material.pgp.Decryption(). + DecryptionKeys(material.keyRingTestPrivate). + VerificationKeys(material.keyRingTestPublic). + VerificationContext(NewVerificationContext(testContext, true, 0)). + New() + testEncryptSplitDecryptStream( + t, + []byte(testMessageString), + nil, + encHandle, + decHandle, + splitWriter, + len(material.keyRingTestPrivate.entities), + Bytes, + ) + }) + } +} + +func TestSessionEncryptDecryptSplitStream(t *testing.T) { + for _, material := range testMaterialForProfiles { + t.Run(material.profileName, func(t *testing.T) { + encHandle, _ := material.pgp.Encryption(). + SessionKey(material.testSessionKey). + SigningKeys(material.keyRingTestPrivate). + New() + decHandle, _ := material.pgp.Decryption(). + SessionKey(material.testSessionKey). + VerificationKeys(material.keyRingTestPublic). + New() + testEncryptSplitDecryptStream( + t, + []byte(testMessageString), + nil, + encHandle, + decHandle, + splitWriter, + len(material.keyRingTestPrivate.entities), + Bytes, + ) + }) + } +} + +func TestSessionEncryptDecryptSplitStreamWithContext(t *testing.T) { + for _, material := range testMaterialForProfiles { + t.Run(material.profileName, func(t *testing.T) { + encHandle, _ := material.pgp.Encryption(). + SessionKey(material.testSessionKey). + SigningKeys(material.keyRingTestPrivate). + SigningContext(NewSigningContext(testContext, true)). + New() + decHandle, _ := material.pgp.Decryption(). + SessionKey(material.testSessionKey). + VerificationKeys(material.keyRingTestPublic). + VerificationContext(NewVerificationContext(testContext, true, 0)). + New() + testEncryptSplitDecryptStream( + t, + []byte(testMessageString), + nil, + encHandle, + decHandle, + splitWriter, + len(material.keyRingTestPrivate.entities), + Bytes, + ) + }) + } +} + +func TestSessionEncryptDecryptSplitStreamWithContextAndCompression(t *testing.T) { + for _, material := range testMaterialForProfiles { + t.Run(material.profileName, func(t *testing.T) { + encHandle, _ := material.pgp.Encryption(). + SessionKey(material.testSessionKey). + SigningKeys(material.keyRingTestPrivate). + SigningContext(NewSigningContext(testContext, true)). + Compress(). + New() + decHandle, _ := material.pgp.Decryption(). + SessionKey(material.testSessionKey). + VerificationKeys(material.keyRingTestPublic). + VerificationContext(NewVerificationContext(testContext, true, 0)). + New() + testEncryptSplitDecryptStream( + t, + []byte(testMessageString), + nil, + encHandle, + decHandle, + splitWriter, + len(material.keyRingTestPrivate.entities), + Bytes, + ) + }) + } +} + +func TestEncryptDecryptDetached(t *testing.T) { + for _, material := range testMaterialForProfiles { + t.Run(material.profileName, func(t *testing.T) { + encHandle, _ := material.pgp.Encryption(). + Recipients(material.keyRingTestPublic). + SigningKeys(material.keyRingTestPrivate). + DetachedSignature(). + New() + decHandle, _ := material.pgp.Decryption(). + DecryptionKeys(material.keyRingTestPrivate). + VerificationKeys(material.keyRingTestPublic). + New() + testEncryptSplitDecryptStream( + t, + []byte(testMessageString), + nil, + encHandle, + decHandle, + splitWriterDetachedSignature, + len(material.keyRingTestPrivate.entities), + Bytes, + ) + }) + } +} + +func TestPasswordEncryptDecryptDetached(t *testing.T) { + for _, material := range testMaterialForProfiles { + t.Run(material.profileName, func(t *testing.T) { + encHandle, _ := material.pgp.Encryption(). + Password(password). + SigningKeys(material.keyRingTestPrivate). + DetachedSignature(). + New() + decHandle, _ := material.pgp.Decryption(). + Passwords(decPasswords). + VerificationKeys(material.keyRingTestPublic). + New() + testEncryptSplitDecryptStream( + t, + []byte(testMessageString), + nil, + encHandle, + decHandle, + splitWriterDetachedSignature, + len(material.keyRingTestPrivate.entities), + Bytes, + ) + }) + } +} + +func TestSessionKeyEncryptDecryptDetached(t *testing.T) { + for _, material := range testMaterialForProfiles { + t.Run(material.profileName, func(t *testing.T) { + encHandle, _ := material.pgp.Encryption(). + SessionKey(material.testSessionKey). + SigningKeys(material.keyRingTestPrivate). + DetachedSignature(). + New() + decHandle, _ := material.pgp.Decryption(). + SessionKey(material.testSessionKey). + VerificationKeys(material.keyRingTestPublic). + New() + testEncryptSplitDecryptStream( + t, + []byte(testMessageString), + nil, + encHandle, + decHandle, + splitWriterDetachedSignature, + len(material.keyRingTestPrivate.entities), + Bytes, + ) + }) + } +} + +func TestEncryptDecryptPlaintextDetached(t *testing.T) { + for _, material := range testMaterialForProfiles { + t.Run(material.profileName, func(t *testing.T) { + encHandle, _ := material.pgp.Encryption(). + Recipients(material.keyRingTestPublic). + SigningKeys(material.keyRingTestPrivate). + PlainDetachedSignature(). + New() + decHandle, _ := material.pgp.Decryption(). + DecryptionKeys(material.keyRingTestPrivate). + VerificationKeys(material.keyRingTestPublic). + PlainDetachedSignature(). + New() + testEncryptSplitDecryptStream( + t, + []byte(testMessageString), + nil, + encHandle, + decHandle, + splitWriterDetachedSignature, + len(material.keyRingTestPrivate.entities), + Bytes, + ) + }) + } +} + +func TestPasswordEncryptDecryptPlaintextDetached(t *testing.T) { + for _, material := range testMaterialForProfiles { + t.Run(material.profileName, func(t *testing.T) { + encHandle, _ := material.pgp.Encryption(). + Password(password). + SigningKeys(material.keyRingTestPrivate). + PlainDetachedSignature(). + New() + decHandle, _ := material.pgp.Decryption(). + Passwords(decPasswords). + VerificationKeys(material.keyRingTestPublic). + PlainDetachedSignature(). + New() + testEncryptSplitDecryptStream( + t, + []byte(testMessageString), + nil, + encHandle, + decHandle, + splitWriterDetachedSignature, + len(material.keyRingTestPrivate.entities), + Bytes, + ) + }) + } +} + +func TestSessionKeyEncryptDecryptPlaintextDetached(t *testing.T) { + for _, material := range testMaterialForProfiles { + t.Run(material.profileName, func(t *testing.T) { + encHandle, _ := material.pgp.Encryption(). + SessionKey(material.testSessionKey). + SigningKeys(material.keyRingTestPrivate). + PlainDetachedSignature(). + New() + decHandle, _ := material.pgp.Decryption(). + SessionKey(material.testSessionKey). + VerificationKeys(material.keyRingTestPublic). + PlainDetachedSignature(). + New() + testEncryptSplitDecryptStream( + t, + []byte(testMessageString), + nil, + encHandle, + decHandle, + splitWriterDetachedSignature, + len(material.keyRingTestPrivate.entities), + Bytes, + ) + }) + } +} + +func TestPasswordEncryptDecryptStream(t *testing.T) { + for _, material := range testMaterialForProfiles { + t.Run(material.profileName, func(t *testing.T) { + encHandle, _ := material.pgp.Encryption(). + Password(password). + New() + decHandle, _ := material.pgp.Decryption(). + Passwords(decPasswords). + New() + testEncryptDecryptStream( + t, + []byte(testMessageUTF8), + nil, + encHandle, + decHandle, + 0, + Bytes, + ) + }) + } +} + +func TestPasswordEncryptSignDecryptStream(t *testing.T) { + for _, material := range testMaterialForProfiles { + t.Run(material.profileName, func(t *testing.T) { + encHandle, _ := material.pgp.Encryption(). + Password(password). + SigningKeys(material.keyRingTestPrivate). + New() + decHandle, _ := material.pgp.Decryption(). + Passwords(decPasswords). + VerificationKeys(material.keyRingTestPublic). + New() + testEncryptDecryptStream( + t, + []byte(testMessageUTF8), + nil, + encHandle, + decHandle, + 0, + Bytes, + ) + }) + } +} + +func TestPasswordEncryptSignDecryptStreamWithCachedSession(t *testing.T) { + for _, material := range testMaterialForProfiles { + t.Run(material.profileName, func(t *testing.T) { + encHandle, _ := material.pgp.Encryption(). + Password(password). + SigningKeys(material.keyRingTestPrivate). + SessionKey(material.testSessionKey). + New() + decHandle, _ := material.pgp.Decryption(). + SessionKey(material.testSessionKey). + VerificationKeys(material.keyRingTestPublic). + New() + testEncryptDecryptStream( + t, + []byte(testMessageUTF8), + nil, + encHandle, + decHandle, + 0, + Bytes, + ) + }) + } +} + +func TestEncryptDecrypt(t *testing.T) { + for _, material := range testMaterialForProfiles { + t.Run(material.profileName, func(t *testing.T) { + encHandle, _ := material.pgp.Encryption(). + Recipients(material.keyRingTestPublic). + SigningKeys(material.keyRingTestPrivate). + New() + decHandle, _ := material.pgp.Decryption(). + DecryptionKeys(material.keyRingTestPrivate). + VerificationKeys(material.keyRingTestPublic). + New() + testEncryptDecrypt( + t, + []byte(testMessage), + nil, + encHandle, + decHandle, + ) + }) + } +} + +func TestEncryptDecryptUTF8(t *testing.T) { + for _, material := range testMaterialForProfiles { + metadata := &LiteralMetadata{ + isUTF8: true, + } + t.Run(material.profileName, func(t *testing.T) { + encHandle, _ := material.pgp.Encryption(). + Recipients(material.keyRingTestPublic). + SigningKeys(material.keyRingTestPrivate). + Utf8(). + New() + decHandle, _ := material.pgp.Decryption(). + DecryptionKeys(material.keyRingTestPrivate). + VerificationKeys(material.keyRingTestPublic). + New() + testEncryptDecrypt( + t, + []byte(testMessageUTF8), + metadata, + encHandle, + decHandle, + ) + }) + } +} + +func TestEncryptDecryptSessionKey(t *testing.T) { + for _, material := range testMaterialForProfiles { + t.Run(material.profileName, func(t *testing.T) { + encHandle, _ := material.pgp.Encryption(). + Recipients(material.keyRingTestPublic). + New() + decHandle, _ := material.pgp.Decryption(). + DecryptionKeys(material.keyRingTestPrivate). + New() + keyPackets, err := encHandle.EncryptSessionKey(material.testSessionKey) + if err != nil { + t.Fatal("Expected no error while generating key packet, got:", err) + } + decryptedSessionKey, err := decHandle.DecryptSessionKey(keyPackets) + if err != nil { + t.Fatal("Expected no error while decrypting key packet, got:", err) + } + if decryptedSessionKey.Algo == "" { + // for v6 algorithm is not encoded in the key + assert.Exactly(t, material.testSessionKey.Key, decryptedSessionKey.Key) + } else { + assert.Exactly(t, material.testSessionKey, decryptedSessionKey) + } + }) + } +} + +func TestEncryptDecryptKey(t *testing.T) { + for _, material := range testMaterialForProfiles { + t.Run(material.profileName, func(t *testing.T) { + key := material.keyRingTestPrivate.GetKeys()[0] + keyLocked, err := material.pgp.LockKey(key, password) + if err != nil { + t.Fatal("Expected no error while encrypting key, got:", err) + } + unlockedKey, err := keyLocked.Unlock(password) + if err != nil { + t.Fatal("Expected no error while decrypting key, got:", err) + } + reflect.DeepEqual(key, unlockedKey) + }) + } +} + +func TestEncryptCompressionApplied(t *testing.T) { + const numReplicas = 10 + builder := strings.Builder{} + for i := 0; i < numReplicas; i++ { + builder.WriteString(testMessage) + } + messageToEncrypt := builder.String() + for _, material := range testMaterialForProfiles { + t.Run(material.profileName, func(t *testing.T) { + encHandleCompress, _ := material.pgp.Encryption(). + Recipients(material.keyRingTestPublic). + SigningKeys(material.keyRingTestPrivate). + Compress(). + New() + + encHandle, _ := material.pgp.Encryption(). + Recipients(material.keyRingTestPublic). + SigningKeys(material.keyRingTestPrivate). + New() + + compressedMessage, err := encHandleCompress.Encrypt([]byte(messageToEncrypt)) + if err != nil { + t.Fatal(err) + } + message, err := encHandle.Encrypt([]byte(messageToEncrypt)) + if err != nil { + t.Fatal(err) + } + if len(compressedMessage.DataPacket) >= len(message.DataPacket) { + t.Fatal("Expected compressed encrypted message to be smaller than the encrypted message") + } + }) + } +} + +func TestEncryptDecryptPlaintextDetachedArmor(t *testing.T) { + for _, material := range testMaterialForProfiles { + t.Run(material.profileName, func(t *testing.T) { + var ciphertextBuf bytes.Buffer + var detachedSignature bytes.Buffer + encHandle, _ := material.pgp.Encryption(). + Recipients(material.keyRingTestPublic). + SigningKeys(material.keyRingTestPrivate). + PlainDetachedSignature(). + New() + decHandle, _ := material.pgp.Decryption(). + DecryptionKeys(material.keyRingTestPrivate). + VerificationKeys(material.keyRingTestPublic). + PlainDetachedSignature(). + New() + writer := NewPGPSplitWriterDetachedSignature(&ciphertextBuf, &detachedSignature) + ctWriter, err := encHandle.EncryptingWriter(writer, Armor) + if err != nil { + t.Fatal("Expected no error while encrypting message, got:", err) + } + if _, err := ctWriter.Write([]byte(testMessage)); err != nil { + t.Fatal(err) + } + if err := ctWriter.Close(); err != nil { + t.Fatal(err) + } + decryptionResult, err := decHandle.DecryptDetached(ciphertextBuf.Bytes(), detachedSignature.Bytes(), Armor) + if err != nil { + t.Fatal("Expected no error while decrypting message, got:", err) + } + if !bytes.Equal(decryptionResult.data, []byte(testMessage)) { + t.Fatalf("Expected the decrypted data to be %s got %s", string(decryptionResult.data), testMessage) + } + if err := decryptionResult.SignatureError(); err != nil { + t.Fatal("Expected no signature error") + } + decHandleNotPlaintext, _ := material.pgp.Decryption(). + DecryptionKeys(material.keyRingTestPrivate). + VerificationKeys(material.keyRingTestPublic). + New() + _, err = decHandleNotPlaintext.DecryptDetached(ciphertextBuf.Bytes(), detachedSignature.Bytes(), Armor) + if err == nil { + t.Fatal("Expected that decrypting an non encrypted plaintext signature fails") + } + }) + } +} + +func TestEncryptArmor(t *testing.T) { + for _, material := range testMaterialForProfiles { + t.Run(material.profileName, func(t *testing.T) { + isV6 := material.keyRingTestPublic.GetKeys()[0].isV6() + encHandle, _ := material.pgp.Encryption(). + Recipients(material.keyRingTestPublic). + SigningKeys(material.keyRingTestPrivate). + New() + pgpMsg, err := encHandle.Encrypt([]byte(testMessageString)) + if err != nil { + t.Fatal("Expected no error in encryption, got:", err) + } + armoredData, err := pgpMsg.Armor() + if err != nil { + t.Fatal("Armoring failed, got:", err) + } + hasChecksum := containsChecksum(armoredData) + if isV6 && hasChecksum { + t.Fatalf("V6 messages should not have a checksum") + } + }) + } +} + +func containsChecksum(armored string) bool { + re := regexp.MustCompile(`=([A-Za-z0-9+/]{4})\s*-----END PGP MESSAGE-----`) + return re.MatchString(armored) +} + +func testEncryptDecrypt( + t *testing.T, + messageBytes []byte, + metadata *LiteralMetadata, + encHandle PGPEncryption, + decHandle PGPDecryption, +) { + expectedMetadata := metadata + pgpMessage, err := encHandle.Encrypt(messageBytes) + if err != nil { + t.Fatal("Expected no error while encrypting with key ring, got:", err) + } + ciphertextBytes := pgpMessage.Bytes() + decryptionResult, err := decHandle.Decrypt(ciphertextBytes, Bytes) + if err != nil { + t.Fatal("Expected no error while calling decrypt with key ring, got:", err) + } + if err = decryptionResult.SignatureError(); err != nil { + t.Fatal("Expected no signature verification error, got:", err) + } + if !bytes.Equal(decryptionResult.Bytes(), messageBytes) { + t.Fatalf("Expected the decrypted data to be %s got %s", string(decryptionResult.Bytes()), string(messageBytes)) + } + decryptedMeta := decryptionResult.Metadata() + if expectedMetadata == nil { + expectedMetadata = &LiteralMetadata{ + filename: metadata.Filename(), + isUTF8: metadata.IsUtf8(), + ModTime: metadata.Time(), + } + } + if !reflect.DeepEqual(expectedMetadata, decryptedMeta) { + t.Fatalf("Expected the decrypted metadata to be %v got %v", metadata, decryptedMeta) + } +} + +func testEncryptSplitDecryptStream( + t *testing.T, + messageBytes []byte, + metadata *LiteralMetadata, //nolint:unparam + encHandle PGPEncryption, + decHandle PGPDecryption, + multiWriterCreator func(Writer, Writer, Writer) PGPSplitWriter, + numberOfSigsToVerify int, + encoding int8, //nolint:unparam +) { + messageReader := bytes.NewReader(messageBytes) + var keyPackets bytes.Buffer + var ciphertextBuf bytes.Buffer + var detachedSignature bytes.Buffer + expectedMetadata := metadata + splitOutput := multiWriterCreator(&keyPackets, &ciphertextBuf, &detachedSignature) + messageWriter, err := encHandle.EncryptingWriter(splitOutput, encoding) + if err != nil { + t.Fatal("Expected no error while encrypting stream with key ring, got:", err) + } + bufferSize := 2 + buffer := make([]byte, bufferSize) + _, err = io.CopyBuffer(messageWriter, messageReader, buffer) + if err != nil { + t.Fatal("Expected no error while copying data, got:", err) + } + err = messageWriter.Close() + if err != nil { + t.Fatal("Expected no error while closing plaintext writer, got:", err) + } + keyPacketsBytes := keyPackets.Bytes() + ciphertextBytes := ciphertextBuf.Bytes() + detachedSignatureBytes := detachedSignature.Bytes() + pgpMessageReader := io.MultiReader( + bytes.NewReader(keyPacketsBytes), + bytes.NewReader(ciphertextBytes), + ) + if len(detachedSignatureBytes) != 0 { + detachedSignatureReader := io.MultiReader( + bytes.NewReader(keyPacketsBytes), + bytes.NewReader(detachedSignatureBytes), + ) + pgpMessageReader = NewPGPSplitReader(pgpMessageReader, detachedSignatureReader) + } + decryptedReader, err := decHandle.DecryptingReader(pgpMessageReader, encoding) + if err != nil { + t.Fatal("Expected no error while calling decrypting stream with key ring, got:", err) + } + _, err = decryptedReader.VerifySignature() + if err == nil { + t.Fatal("Expected verify error not read all, got nil") + } + decryptedBytes, err := io.ReadAll(decryptedReader) + if err != nil { + t.Fatal("Expected no error while reading the decrypted data, got:", err) + } + if !bytes.Equal(decryptedBytes, messageBytes) { + t.Fatalf("Expected the decrypted data to be %s got %s", string(decryptedBytes), string(messageBytes)) + } + if numberOfSigsToVerify > 0 { + verifyResult, err := decryptedReader.VerifySignature() + if err != nil { + t.Fatal("Expected no error, got:", err) + } + if err = verifyResult.SignatureError(); err != nil { + t.Fatal("Expected no error while verifying the signature, got:", err) + } + if len(verifyResult.Signatures) != numberOfSigsToVerify { + t.Fatalf("Not enough signatures verified, should be %d", numberOfSigsToVerify) + } + for _, verifiedSignature := range verifyResult.Signatures { + if verifiedSignature.SignatureError != nil { + t.Fatal("One of the contained signatures did not correctly verify ", verifiedSignature.SignatureError.Message) + } + } + } + decryptedMeta := decryptedReader.GetMetadata() + if expectedMetadata == nil { + expectedMetadata = &LiteralMetadata{ + filename: metadata.Filename(), + isUTF8: metadata.IsUtf8(), + ModTime: metadata.Time(), + } + } + if !reflect.DeepEqual(expectedMetadata, decryptedMeta) { + t.Fatalf("Expected the decrypted metadata to be %v got %v", metadata, decryptedMeta) + } +} + +func testEncryptDecryptStream( + t *testing.T, + messageBytes []byte, + metadata *LiteralMetadata, + encHandle PGPEncryption, + decHandle PGPDecryption, + numberOfSigsToVerify int, + encoding int8, +) { + messageReader := bytes.NewReader(messageBytes) + var ciphertextBuf bytes.Buffer + expectedMetadata := metadata + messageWriter, err := encHandle.EncryptingWriter(&ciphertextBuf, encoding) + if err != nil { + t.Fatal("Expected no error while encrypting stream with key ring, got:", err) + } + bufferSize := 2 + buffer := make([]byte, bufferSize) + _, err = io.CopyBuffer(messageWriter, messageReader, buffer) + if err != nil { + t.Fatal("Expected no error while copying data, got:", err) + } + err = messageWriter.Close() + if err != nil { + t.Fatal("Expected no error while closing plaintext writer, got:", err) + } + ciphertextBytes := ciphertextBuf.Bytes() + decryptedReader, err := decHandle.DecryptingReader(bytes.NewReader(ciphertextBytes), encoding) + if err != nil { + t.Fatal("Expected no error while calling decrypting stream with key ring, got:", err) + } + _, err = decryptedReader.VerifySignature() + if err == nil { + t.Fatal("Expected an error while verifying the signature before reading the data, got nil") + } + decryptedBytes, err := io.ReadAll(decryptedReader) + if err != nil { + t.Fatal("Expected no error while reading the decrypted data, got:", err) + } + if !bytes.Equal(decryptedBytes, messageBytes) { + t.Fatalf("Expected the decrypted data to be %s got %s", string(decryptedBytes), string(messageBytes)) + } + if numberOfSigsToVerify > 0 { + verifyResult, err := decryptedReader.VerifySignature() + if err != nil { + t.Fatal("Expected no error while verifying the signature, got:", err) + } + if err = verifyResult.SignatureError(); err != nil { + t.Fatal("Expected no signature error while verifying the signature, got:", err) + } + if len(verifyResult.Signatures) != numberOfSigsToVerify { + t.Fatalf("Not enough signatures verified, should be %d", numberOfSigsToVerify) + } + for _, verifiedSignature := range verifyResult.Signatures { + if verifiedSignature.SignatureError != nil { + t.Fatal("One of the contained signatures did not correctly verify ", verifiedSignature.SignatureError.Message) + } + } + } + decryptedMeta := decryptedReader.GetMetadata() + if expectedMetadata == nil { + expectedMetadata = &LiteralMetadata{ + filename: metadata.Filename(), + isUTF8: metadata.IsUtf8(), + ModTime: metadata.Time(), + } + } + if !reflect.DeepEqual(expectedMetadata, decryptedMeta) { + t.Fatalf("Expected the decrypted metadata to be %v got %v", metadata, decryptedMeta) + } +} + +func splitWriter(w1 Writer, w2 Writer, w3 Writer) PGPSplitWriter { + return NewPGPSplitWriterKeyAndData(w1, w2) +} + +func splitWriterDetachedSignature(w1 Writer, w2 Writer, w3 Writer) PGPSplitWriter { + return NewPGPSplitWriter(w1, w2, w3) +} diff --git a/crypto/encryption.go b/crypto/encryption.go new file mode 100644 index 00000000..235eeede --- /dev/null +++ b/crypto/encryption.go @@ -0,0 +1,49 @@ +package crypto + +import "github.com/ProtonMail/go-crypto/openpgp/packet" + +type EncryptionProfile interface { + EncryptionConfig() *packet.Config + CompressionConfig() *packet.Config +} + +// PGPEncryption is an interface for encrypting messages with GopenPGP. +// Use an EncryptionHandleBuilder to create a PGPEncryption handle. +type PGPEncryption interface { + // EncryptingWriter returns a wrapper around underlying output Writer, + // such that any write-operation via the wrapper results in a write to an encrypted pgp message. + // If the output Writer is of type PGPSplitWriter, the output can be split to multiple writers + // for different parts of the message. For example to write key packets and encrypted data packets + // to different writers or to write a detached signature separately. + // The encoding argument defines the output encoding, i.e., Bytes or Armored + // The returned pgp message WriteCloser must be closed after the plaintext has been written. + EncryptingWriter(output Writer, encoding int8) (WriteCloser, error) + // Encrypt encrypts a plaintext message. + Encrypt(message []byte) (*PGPMessage, error) + // EncryptSessionKey encrypts a session key with the encryption handle. + // To encrypt a session key, the handle must contain either recipients or a password. + EncryptSessionKey(sessionKey *SessionKey) ([]byte, error) + // ClearPrivateParams clears all private key material contained in EncryptionHandle from memory. + ClearPrivateParams() +} + +// Writer replicates the io.Writer interface for go-mobile. +type Writer interface { + Write(b []byte) (n int, err error) +} + +// WriteCloser replicates the io.WriteCloser interface for go-mobile. +type WriteCloser interface { + Write(b []byte) (n int, err error) + Close() (err error) +} + +// PGPSplitWriter is an interface to write different parts of a PGP message +// (i.e., packets) to different streams. +type PGPSplitWriter interface { + Writer + // Keys returns the Writer to which the key packets are written to. + Keys() Writer + // Signature returns the Writer to which an encrypted detached signature is written to. + Signature() Writer +} diff --git a/crypto/encryption_core.go b/crypto/encryption_core.go new file mode 100644 index 00000000..89fe1acc --- /dev/null +++ b/crypto/encryption_core.go @@ -0,0 +1,429 @@ +package crypto + +import ( + "io" + "time" + + "github.com/ProtonMail/go-crypto/openpgp/packet" + openpgp "github.com/ProtonMail/go-crypto/openpgp/v2" + "github.com/ProtonMail/gopenpgp/v3/constants" + "github.com/ProtonMail/gopenpgp/v3/internal" + "github.com/pkg/errors" +) + +// pgpSplitWriter type implements the PGPSplitWriter +// interface. +type pgpSplitWriter struct { + keyPackets Writer + ciphertext Writer + detachedSignature Writer +} + +// pgpSplitWriter implements the PGPSplitWriter interface + +func (mw *pgpSplitWriter) Keys() Writer { + return mw.keyPackets +} + +func (mw *pgpSplitWriter) Write(b []byte) (int, error) { + return mw.ciphertext.Write(b) +} + +func (mw *pgpSplitWriter) Signature() Writer { + return mw.detachedSignature +} + +// NewPGPSplitWriter creates a type that implements the PGPSplitWriter interface +// for encrypting a plaintext where the output PGP packets should be written to the different streams provided. +// Key packets are written to keyPackets whereas the encrypted data packets are written to encPackets. +// The encrypted detached signature data is written to encSigPacket. +func NewPGPSplitWriter(keyPackets Writer, encPackets Writer, encSigPacket Writer) PGPSplitWriter { + return &pgpSplitWriter{ + keyPackets: keyPackets, + ciphertext: encPackets, + detachedSignature: encSigPacket, + } +} + +// NewPGPSplitWriterKeyAndData creates a type that implements the PGPSplitWriter interface +// for encrypting a plaintext where the output PGP packets should be written to the different streams provided. +// Key packets are written to keyPackets whereas the encrypted data packets are written to encPackets. +func NewPGPSplitWriterKeyAndData(keyPackets Writer, encPackets Writer) PGPSplitWriter { + return NewPGPSplitWriter(keyPackets, encPackets, nil) +} + +// NewPGPSplitWriterDetachedSignature creates a type that implements the PGPSplitWriter interface +// for encrypting a plaintext where the output PGP messages should be written to the different streams provided. +// The encrypted data message is written to encMessage whereas the encrypted detached signature is written to +// encSigMessage. +func NewPGPSplitWriterDetachedSignature(encMessage Writer, encSigMessage Writer) PGPSplitWriter { + return NewPGPSplitWriter(nil, encMessage, encSigMessage) +} + +// NewPGPSplitWriterFromWriter creates a type that implements the PGPSplitWriter interface +// for encrypting a plaintext where the output PGP messages to the provided Writer. +func NewPGPSplitWriterFromWriter(writer Writer) PGPSplitWriter { + return NewPGPSplitWriter(writer, writer, nil) +} + +type signAndEncryptWriteCloser struct { + signWriter WriteCloser + encryptWriter WriteCloser +} + +func (w *signAndEncryptWriteCloser) Write(b []byte) (int, error) { + return w.signWriter.Write(b) +} + +func (w *signAndEncryptWriteCloser) Close() error { + if err := w.signWriter.Close(); err != nil { + return err + } + return w.encryptWriter.Close() +} + +func (eh *encryptionHandle) prepareEncryptAndSign( + plainMessageMetadata *LiteralMetadata, +) (hints *openpgp.FileHints, config *packet.Config, signEntities []*openpgp.Entity, err error) { + hints = &openpgp.FileHints{ + FileName: plainMessageMetadata.Filename(), + IsUTF8: eh.IsUTF8, + ModTime: time.Unix(plainMessageMetadata.Time(), 0), + } + + config = eh.profile.EncryptionConfig() + config.Time = eh.clock + + compressionConfig := eh.selectCompression() + config.DefaultCompressionAlgo = compressionConfig.DefaultCompressionAlgo + config.CompressionConfig = compressionConfig.CompressionConfig + + if eh.SigningContext != nil { + config.SignatureNotations = append(config.SignatureNotations, eh.SigningContext.getNotation()) + } + + if eh.SignKeyRing != nil && len(eh.SignKeyRing.entities) > 0 { + signEntities, err = eh.SignKeyRing.signingEntities() + if err != nil { + return + } + } + return +} + +func (eh *encryptionHandle) encryptStream( + keyPacketWriter Writer, + dataPacketWriter Writer, + plainMessageMetadata *LiteralMetadata, +) (plainMessageWriter WriteCloser, err error) { + var sessionKeyBytes []byte + var additionalPasswords [][]byte + if eh.SessionKey != nil { + sessionKeyBytes = eh.SessionKey.Key + } + if eh.Password != nil { + additionalPasswords = [][]byte{eh.Password} + } + hints, config, signers, err := eh.prepareEncryptAndSign(plainMessageMetadata) + if err != nil { + return nil, err + } + var encryptionTimeOverride *time.Time + if eh.encryptionTimeOverride != nil { + encryptionTime := eh.encryptionTimeOverride() + encryptionTimeOverride = &encryptionTime + } + plainMessageWriter, err = openpgp.EncryptWithParams( + dataPacketWriter, + eh.Recipients.getEntities(), + eh.HiddenRecipients.getEntities(), + &openpgp.EncryptParams{ + KeyWriter: keyPacketWriter, + Signers: signers, + Hints: hints, + SessionKey: sessionKeyBytes, + Passwords: additionalPasswords, + Config: config, + TextSig: eh.IsUTF8, + OutsideSig: eh.ExternalSignature, + EncryptionTime: encryptionTimeOverride, + }, + ) + if err != nil { + return nil, errors.Wrap(err, "gopenpgp: error in encrypting asymmetrically") + } + return plainMessageWriter, nil +} + +func (eh *encryptionHandle) encryptStreamWithPassword( + keyPacketWriter Writer, + dataPacketWriter Writer, + plainMessageMetadata *LiteralMetadata, +) (plainMessageWriter io.WriteCloser, err error) { + var sessionKeyBytes []byte + if eh.SessionKey != nil { + sessionKeyBytes = eh.SessionKey.Key + } + hints, config, signers, err := eh.prepareEncryptAndSign(plainMessageMetadata) + if err != nil { + return + } + plainMessageWriter, err = openpgp.SymmetricallyEncryptWithParams( + eh.Password, + dataPacketWriter, + &openpgp.EncryptParams{ + KeyWriter: keyPacketWriter, + Signers: signers, + Hints: hints, + SessionKey: sessionKeyBytes, + Config: config, + TextSig: eh.IsUTF8, + OutsideSig: eh.ExternalSignature, + }, + ) + if err != nil { + return nil, errors.Wrap(err, "gopenpgp: error in encrypting asymmetrically") + } + return plainMessageWriter, nil +} + +func (eh *encryptionHandle) encryptStreamWithSessionKey( + dataPacketWriter Writer, + plainMessageMetadata *LiteralMetadata, +) (plainMessageWriter WriteCloser, err error) { + encryptWriter, signWriter, err := eh.encryptStreamWithSessionKeyHelper( + plainMessageMetadata, + dataPacketWriter, + ) + + if err != nil { + return nil, err + } + if signWriter != nil { + plainMessageWriter = &signAndEncryptWriteCloser{signWriter, encryptWriter} + } else { + plainMessageWriter = encryptWriter + } + return plainMessageWriter, err +} + +func (eh *encryptionHandle) encryptStreamWithSessionKeyHelper( + plainMessageMetadata *LiteralMetadata, + dataPacketWriter io.Writer, +) (encryptWriter, signWriter io.WriteCloser, err error) { + hints, config, signers, err := eh.prepareEncryptAndSign(plainMessageMetadata) + if err != nil { + return nil, nil, err + } + + if !eh.SessionKey.v6 { + config.DefaultCipher, err = eh.SessionKey.GetCipherFunc() + if err != nil { + return nil, nil, errors.Wrap(err, "gopenpgp: unable to encrypt with session key") + } + } + + encryptWriter, err = packet.SerializeSymmetricallyEncrypted( + dataPacketWriter, + config.Cipher(), + config.AEAD() != nil, + packet.CipherSuite{Cipher: config.Cipher(), Mode: config.AEAD().Mode()}, + eh.SessionKey.Key, + config, + ) + + if err != nil { + return nil, nil, errors.Wrap(err, "gopenpgp: unable to encrypt") + } + + if algo := config.Compression(); algo != packet.CompressionNone { + encryptWriter, err = packet.SerializeCompressed(encryptWriter, algo, config.CompressionConfig) + if err != nil { + return nil, nil, errors.Wrap(err, "gopenpgp: error in compression") + } + } + + if signers != nil { + signWriter, err = openpgp.SignWithParams(encryptWriter, signers, &openpgp.SignParams{ + Hints: hints, + TextSig: eh.IsUTF8, + OutsideSig: eh.ExternalSignature, + Config: config, + }) + if err != nil { + return nil, nil, errors.Wrap(err, "gopenpgp: unable to sign") + } + } else { + encryptWriter, err = packet.SerializeLiteral( + encryptWriter, + !plainMessageMetadata.IsUtf8(), + plainMessageMetadata.Filename(), + uint32(plainMessageMetadata.Time()), + ) + if err != nil { + return nil, nil, errors.Wrap(err, "gopenpgp: unable to serialize") + } + } + return encryptWriter, signWriter, nil +} + +type encryptSignDetachedWriter struct { + ptToCiphertextWriter WriteCloser + sigToCiphertextWriter WriteCloser + ptToEncSigWriter WriteCloser + ptWriter Writer +} + +func (w *encryptSignDetachedWriter) Write(b []byte) (int, error) { + return w.ptWriter.Write(b) +} + +func (w *encryptSignDetachedWriter) Close() error { + if err := w.ptToCiphertextWriter.Close(); err != nil { + return err + } + if err := w.ptToEncSigWriter.Close(); err != nil { + return err + } + return w.sigToCiphertextWriter.Close() +} + +// encryptSignDetachedStreamWithSessionKey wraps writers to encrypt a message +// with a session key and produces a detached signature for the plaintext. +func (eh *encryptionHandle) encryptSignDetachedStreamWithSessionKey( + plainMessageMetadata *LiteralMetadata, + encryptedSignatureWriter io.Writer, + encryptedDataWriter io.Writer, + encryptSignature bool, +) (io.WriteCloser, error) { + signKeyRing := eh.SignKeyRing + eh.SignKeyRing = nil + defer func() { + eh.SignKeyRing = signKeyRing + }() + // Create a writer to encrypt the message. + ptToCiphertextWriter, err := eh.encryptStreamWithSessionKey(encryptedDataWriter, plainMessageMetadata) + if err != nil { + return nil, err + } + var sigToCiphertextWriter io.WriteCloser + if encryptSignature { + // Create a writer to encrypt the signature. + sigToCiphertextWriter, err = eh.encryptStreamWithSessionKey(encryptedSignatureWriter, nil) + if err != nil { + return nil, err + } + } else { + // The signature is not encrypted + sigToCiphertextWriter = internal.NewNoOpWriteCloser(encryptedSignatureWriter) + } + // Create a writer to sign the message. + ptToEncSigWriter, err := signMessageDetachedWriter( + signKeyRing, + sigToCiphertextWriter, + eh.IsUTF8, + eh.SigningContext, + eh.clock, + eh.profile.EncryptionConfig(), + ) + if err != nil { + return nil, err + } + // Return a wrapped plaintext writer that writes encrypted data and the encrypted signature. + return &encryptSignDetachedWriter{ + ptToCiphertextWriter: ptToCiphertextWriter, + sigToCiphertextWriter: sigToCiphertextWriter, + ptToEncSigWriter: ptToEncSigWriter, + ptWriter: io.MultiWriter(ptToCiphertextWriter, ptToEncSigWriter), + }, nil +} + +func (eh *encryptionHandle) encryptSignDetachedStreamToRecipients( + plainMessageMetadata *LiteralMetadata, + encryptedSignatureWriter io.Writer, + encryptedDataWriter io.Writer, + keyPacketWriter io.Writer, + encryptSignature bool, +) (plaintextWriter io.WriteCloser, err error) { + configInput := eh.profile.EncryptionConfig() + configInput.Time = NewConstantClock(eh.clock().Unix()) + // Generate a session key for encryption. + if eh.SessionKey == nil { + eh.SessionKey, err = generateSessionKey(configInput) + if err != nil { + return nil, err + } + defer func() { + eh.SessionKey.Clear() + eh.SessionKey = nil + }() + } + if keyPacketWriter == nil { + // If no separate keyPacketWriter is given, write the key packets + // as prefix to the encrypted data and encrypted signature. + keyPacketWriter = io.MultiWriter(encryptedDataWriter, encryptedSignatureWriter) + } + + encryptionTimeOverride := configInput.Now() + if eh.encryptionTimeOverride != nil { + encryptionTimeOverride = eh.encryptionTimeOverride() + } + if eh.Recipients != nil || eh.HiddenRecipients != nil { + // Encrypt the session key to the different recipients. + if err = encryptSessionKeyToWriter( + eh.Recipients, + eh.HiddenRecipients, + eh.SessionKey, + keyPacketWriter, + encryptionTimeOverride, + configInput, + ); err != nil { + return nil, err + } + } + if eh.Password != nil { + // If not recipients present use the provided password + if err = encryptSessionKeyWithPasswordToWriter( + eh.Password, + eh.SessionKey, + keyPacketWriter, + configInput, + ); err != nil { + return nil, err + } + } + if eh.Password == nil && eh.Recipients == nil && eh.HiddenRecipients == nil { + return nil, errors.New("openpgp: no key material to encrypt") + } + + // Use the session key to encrypt message + signature of the message. + plaintextWriter, err = eh.encryptSignDetachedStreamWithSessionKey( + plainMessageMetadata, + encryptedSignatureWriter, + encryptedDataWriter, + encryptSignature, + ) + if err != nil { + return nil, err + } + return plaintextWriter, err +} + +func (eh *encryptionHandle) selectCompression() (config *packet.Config) { + config = &packet.Config{} + switch eh.Compression { + case constants.DefaultCompression: + config = eh.profile.CompressionConfig() + case constants.ZIPCompression: + config.DefaultCompressionAlgo = packet.CompressionZIP + config.CompressionConfig = &packet.CompressionConfig{ + Level: 6, + } + case constants.ZLIBCompression: + config.DefaultCompressionAlgo = packet.CompressionZLIB + config.CompressionConfig = &packet.CompressionConfig{ + Level: 6, + } + } + return config +} diff --git a/crypto/encryption_handle.go b/crypto/encryption_handle.go new file mode 100644 index 00000000..9cbbcabe --- /dev/null +++ b/crypto/encryption_handle.go @@ -0,0 +1,348 @@ +package crypto + +import ( + "io" + + "github.com/ProtonMail/go-crypto/openpgp/armor" + openpgp "github.com/ProtonMail/go-crypto/openpgp/v2" + "github.com/ProtonMail/gopenpgp/v3/constants" + "github.com/ProtonMail/gopenpgp/v3/internal" + "github.com/pkg/errors" +) + +// encryptionHandle collects the configuration parameters for encrypting a message. +// Use a encryptionHandleBuilder to create a handle. +type encryptionHandle struct { + // Recipients contains the public keys to which + // the message should be encrypted to. + // Triggers hybrid encryption with public keys of the recipients and hidden recipients. + // The recipients are included in the intended recipient fingerprint list + // of the signature, if a signature is present. + // If nil, set another field for the type of encryption: HiddenRecipients, SessionKey, or Password + Recipients *KeyRing + // HiddenRecipients contains the public keys to which + // the message should be encrypted to. + // Triggers hybrid encryption with public keys of the recipients and hidden recipients. + // The hidden recipients are NOT included in the intended recipient fingerprint list + // of the signature, if a signature is present. + // If nil, set another field for the type of encryption: Recipients, SessionKey, or Password + HiddenRecipients *KeyRing + // SessionKey defines the session key the message should be encrypted with. + // Triggers session key encryption with the included session key. + // If nil, set another field for the type of encryption: Recipients, HiddenRecipients, or Password + SessionKey *SessionKey + // Password defines a password the message should be encrypted with. + // Triggers password based encryption with a key derived from the password. + // If nil, set another field for the type of encryption: Recipients, HiddenRecipients, or SessionKey + Password []byte + // SignKeyRing provides an unlocked key ring to include signature in the message. + // If nil, no signature is included. + SignKeyRing *KeyRing + // SigningContext provides a signing context for the signature in the message. + // SignKeyRing has to be set if a SigningContext is provided. + SigningContext *SigningContext + // ArmorHeaders provides armor headers if the message is armored. + // Only considered if Armored is set to true. + ArmorHeaders map[string]string + // Compression indicates if the plaintext should be compressed before encryption. + // constants.NoCompression: none, constants.DefaultCompression: profile default + // constants.ZIPCompression: zip, constants.ZLIBCompression: zlib + Compression int8 + // DetachedSignature indicates if a separate encrypted detached signature + // should be created + DetachedSignature bool + // PlainDetachedSignature indicates that the detached signature should not be encrypted. + // Is only considered if DetachedSignature is not set. + PlainDetachedSignature bool + IsUTF8 bool + // ExternalSignature allows to include an external signature into + // the encrypted message. + ExternalSignature []byte + profile EncryptionProfile + + encryptionTimeOverride Clock + clock Clock +} + +// --- Default decryption handle to build from + +func defaultEncryptionHandle(profile EncryptionProfile, clock Clock) *encryptionHandle { + return &encryptionHandle{ + profile: profile, + clock: clock, + } +} + +// --- Implements PGPEncryption interface + +// EncryptingWriter returns a wrapper around underlying output Writer, +// such that any write-operation via the wrapper results in a write to an encrypted pgp message. +// If the output Writer is of type PGPSplitWriter, the output can be split to multiple writers +// for different parts of the message. For example to write key packets and encrypted data packets +// to different writers or to write a detached signature separately. +// The encoding argument defines the output encoding, i.e., Bytes or Armored +// The returned pgp message WriteCloser must be closed after the plaintext has been written. +func (eh *encryptionHandle) EncryptingWriter(outputWriter Writer, encoding int8) (messageWriter WriteCloser, err error) { + pgpSplitWriter := castToPGPSplitWriter(outputWriter) + if pgpSplitWriter != nil { + return eh.encryptingWriters(pgpSplitWriter.Keys(), pgpSplitWriter, pgpSplitWriter.Signature(), nil, armorOutput(encoding)) + } + if eh.DetachedSignature { + return nil, errors.New("gopenpgp: no pgp split writer provided for the detached signature") + } + return eh.encryptingWriters(nil, outputWriter, nil, nil, armorOutput(encoding)) +} + +// Encrypt encrypts a plaintext message. +func (eh *encryptionHandle) Encrypt(message []byte) (*PGPMessage, error) { + pgpMessageBuffer := NewPGPMessageBuffer() + // Enforce that for a PGPMessage struct the output should not be armored. + encryptingWriter, err := eh.EncryptingWriter(pgpMessageBuffer, Bytes) + if err != nil { + return nil, err + } + _, err = encryptingWriter.Write(message) + if err != nil { + return nil, err + } + err = encryptingWriter.Close() + if err != nil { + return nil, err + } + checksum := eh.armorChecksumRequired() + return pgpMessageBuffer.PGPMessageWithOptions(eh.PlainDetachedSignature, !checksum), nil +} + +// EncryptSessionKey encrypts a session key with the encryption handle. +// To encrypt a session key, the handle must contain either recipients or a password. +func (eh *encryptionHandle) EncryptSessionKey(sessionKey *SessionKey) ([]byte, error) { + config := eh.profile.EncryptionConfig() + config.Time = NewConstantClock(eh.clock().Unix()) + switch { + case eh.Password != nil: + return encryptSessionKeyWithPassword(sessionKey, eh.Password, config) + case eh.Recipients != nil || eh.HiddenRecipients != nil: + encryptionTimeOverride := config.Now() + if eh.encryptionTimeOverride != nil { + encryptionTimeOverride = eh.encryptionTimeOverride() + } + return encryptSessionKey(eh.Recipients, eh.HiddenRecipients, sessionKey, encryptionTimeOverride, config) + } + return nil, errors.New("gopenpgp: no password or recipients in encryption handle") +} + +// --- Helper methods on encryption handle + +func (eh *encryptionHandle) validate() error { + if eh.Recipients == nil && + eh.HiddenRecipients == nil && + eh.Password == nil && + eh.SessionKey == nil { + return errors.New("gopenpgp: no encryption key material provided") + } + + if eh.SignKeyRing == nil && eh.SigningContext != nil { + return errors.New("gopenpgp: no signing key but signing context provided") + } + + if eh.SignKeyRing == nil && eh.DetachedSignature { + return errors.New("gopenpgp: no signing key provided for detached signature") + } + return nil +} + +// armorChecksumRequired determines if an armor checksum should be appended or not. +// The OpenPGP Crypto-Refresh mandates that no checksum should be appended with the new packets. +func (eh *encryptionHandle) armorChecksumRequired() bool { + if !constants.ArmorChecksumEnabled { + // If the default behavior is no checksum, we can ignore + // the logic for the crypto refresh check. + return false + } + encryptionConfig := eh.profile.EncryptionConfig() + if encryptionConfig.AEADConfig == nil { + return true + } + checkTime := eh.clock() + if eh.Recipients != nil { + for _, recipient := range eh.Recipients.entities { + primarySelfSignature, err := recipient.PrimarySelfSignature(checkTime, encryptionConfig) + if err != nil { + return true + } + if !primarySelfSignature.SEIPDv2 { + return true + } + } + } + if eh.HiddenRecipients != nil { + for _, recipient := range eh.HiddenRecipients.entities { + primarySelfSignature, err := recipient.PrimarySelfSignature(checkTime, encryptionConfig) + if err != nil { + return true + } + if !primarySelfSignature.SEIPDv2 { + return true + } + } + } + return false +} + +type armoredWriteCloser struct { + armorWriter WriteCloser + messageWriter WriteCloser + armorSigWriter WriteCloser +} + +func (w *armoredWriteCloser) Write(b []byte) (int, error) { + return w.messageWriter.Write(b) +} + +func (w *armoredWriteCloser) Close() error { + if err := w.messageWriter.Close(); err != nil { + return err + } + if w.armorSigWriter != nil { + if err := w.armorSigWriter.Close(); err != nil { + return err + } + } + return w.armorWriter.Close() +} + +// ClearPrivateParams clears all private key material contained in EncryptionHandle from memory. +func (eh *encryptionHandle) ClearPrivateParams() { + if eh.SignKeyRing != nil { + eh.SignKeyRing.ClearPrivateParams() + } + if eh.SessionKey != nil { + eh.SessionKey.Clear() + } + if eh.Password != nil { + clearMem(eh.Password) + } +} + +func (eh *encryptionHandle) handleArmor(keys, data, detachedSignature Writer) ( + dataOut Writer, + detachedSignatureOut Writer, + armorWriter WriteCloser, + armorSigWriter WriteCloser, + err error, +) { + writeChecksum := eh.armorChecksumRequired() + detachedSignatureOut = detachedSignature + // Wrap armored writer + if eh.ArmorHeaders == nil { + eh.ArmorHeaders = internal.ArmorHeaders + } + armorWriter, err = armor.EncodeWithChecksumOption(data, constants.PGPMessageHeader, eh.ArmorHeaders, writeChecksum) + dataOut = armorWriter + if err != nil { + return nil, nil, nil, nil, err + } + if eh.DetachedSignature { + armorSigWriter, err = armor.EncodeWithChecksumOption(detachedSignature, constants.PGPMessageHeader, eh.ArmorHeaders, writeChecksum) + detachedSignatureOut = armorSigWriter + if err != nil { + return nil, nil, nil, nil, err + } + } else if eh.PlainDetachedSignature { + armorSigWriter, err = armor.EncodeWithChecksumOption(detachedSignature, constants.PGPSignatureHeader, eh.ArmorHeaders, writeChecksum) + detachedSignatureOut = armorSigWriter + if err != nil { + return nil, nil, nil, nil, err + } + } + if keys != nil { + return nil, nil, nil, nil, errors.New("gopenpgp: armor is not allowed if key packets are written separately") + } + return dataOut, detachedSignatureOut, armorWriter, armorSigWriter, nil +} + +func (eh *encryptionHandle) encryptingWriters(keys, data, detachedSignature Writer, meta *LiteralMetadata, armorOutput bool) (messageWriter WriteCloser, err error) { + var armorWriter WriteCloser + var armorSigWriter WriteCloser + if err = eh.validate(); err != nil { + return nil, err + } + + doDetachedSignature := eh.DetachedSignature || eh.PlainDetachedSignature + if doDetachedSignature && detachedSignature == nil { + return nil, errors.New("gopenpgp: no output provided for the detached signature") + } + + if armorOutput { + data, detachedSignature, armorWriter, armorSigWriter, err = eh.handleArmor(keys, data, detachedSignature) + if err != nil { + return nil, err + } + } + if keys == nil { + // No writer for key packets provided, + // write the key packets at the beginning of each message. + if eh.DetachedSignature { + keys = io.MultiWriter(data, detachedSignature) + } else { + keys = data + } + } + switch { + case eh.Recipients.CountEntities() > 0 || eh.HiddenRecipients.CountEntities() > 0: + // Encrypt towards recipients + if !doDetachedSignature { + // Signature is inside the ciphertext. + messageWriter, err = eh.encryptStream(keys, data, meta) + } else { + // Encrypted detached signature separate from the ciphertext. + messageWriter, err = eh.encryptSignDetachedStreamToRecipients(meta, detachedSignature, data, keys, eh.DetachedSignature) + } + case eh.Password != nil: + // Encrypt with a password + if !doDetachedSignature { + messageWriter, err = eh.encryptStreamWithPassword(keys, data, meta) + } else { + messageWriter, err = eh.encryptSignDetachedStreamToRecipients(meta, detachedSignature, data, keys, eh.DetachedSignature) + } + case eh.SessionKey != nil: + // Encrypt towards session key + if !doDetachedSignature { + messageWriter, err = eh.encryptStreamWithSessionKey(data, meta) + } else { + messageWriter, err = eh.encryptSignDetachedStreamWithSessionKey(meta, detachedSignature, data, eh.DetachedSignature) + } + default: + // No encryption material provided + err = errors.New("gopenpgp: no encryption key ring, session key, or password provided") + } + if err != nil { + return nil, err + } + if armorOutput { + // Wrap armored writer + messageWriter = &armoredWriteCloser{ + armorWriter: armorWriter, + messageWriter: messageWriter, + armorSigWriter: armorSigWriter, + } + } + if eh.IsUTF8 { + messageWriter = internal.NewUtf8CheckWriteCloser( + openpgp.NewCanonicalTextWriteCloser(messageWriter), + ) + } + return messageWriter, nil +} + +func castToPGPSplitWriter(w Writer) PGPSplitWriter { + v, ok := interface{}(w).(PGPSplitWriter) + if ok { + return v + } + v, ok = interface{}(&w).(PGPSplitWriter) + if ok { + return v + } + return nil +} diff --git a/crypto/encryption_handle_builder.go b/crypto/encryption_handle_builder.go new file mode 100644 index 00000000..9f8b18d9 --- /dev/null +++ b/crypto/encryption_handle_builder.go @@ -0,0 +1,216 @@ +package crypto + +import "github.com/ProtonMail/gopenpgp/v3/constants" + +// EncryptionHandleBuilder allows to configure a decryption handle to decrypt an OpenPGP message. +type EncryptionHandleBuilder struct { + handle *encryptionHandle + defaultClock Clock + err error +} + +func newEncryptionHandleBuilder(profile EncryptionProfile, clock Clock) *EncryptionHandleBuilder { + return &EncryptionHandleBuilder{ + handle: defaultEncryptionHandle(profile, clock), + defaultClock: clock, + } +} + +// Recipient sets the public key to which the message should be encrypted to. +// Triggers hybrid encryption with public keys of the recipients and hidden recipients. +// The recipients are included in the intended recipient fingerprint list +// of the signature, if a signature is present. +// If not set, set another type of encryption: HiddenRecipients, SessionKey, or Password. +func (ehb *EncryptionHandleBuilder) Recipient(key *Key) *EncryptionHandleBuilder { + var err error + if ehb.handle.Recipients == nil { + ehb.handle.Recipients, err = NewKeyRing(key) + } else { + err = ehb.handle.Recipients.AddKey(key) + } + ehb.err = err + return ehb +} + +// Recipients sets the public keys to which the message should be encrypted to. +// Triggers hybrid encryption with public keys of the recipients and hidden recipients. +// The recipients are included in the intended recipient fingerprint list +// of the signature, if a signature is present. +// If not set, set another type of encryption: HiddenRecipients, SessionKey, or Password. +func (ehb *EncryptionHandleBuilder) Recipients(recipients *KeyRing) *EncryptionHandleBuilder { + ehb.handle.Recipients = recipients + return ehb +} + +// HiddenRecipient sets a public key to which the message should be encrypted to. +// Triggers hybrid encryption with public keys of the recipients and hidden recipients. +// The hidden recipients are NOT included in the intended recipient fingerprint list +// of the signature, if a signature is present. +// If not set, set another type of encryption: Recipients, SessionKey, or Password. +func (ehb *EncryptionHandleBuilder) HiddenRecipient(key *Key) *EncryptionHandleBuilder { + var err error + if ehb.handle.HiddenRecipients == nil { + ehb.handle.HiddenRecipients, err = NewKeyRing(key) + } else { + err = ehb.handle.HiddenRecipients.AddKey(key) + } + ehb.err = err + return ehb +} + +// HiddenRecipients sets the public keys to which the message should be encrypted to. +// Triggers hybrid encryption with public keys of the recipients and hidden recipients. +// The hidden recipients are NOT included in the intended recipient fingerprint list +// of the signature, if a signature is present. +// If not set, set another type of encryption: Recipients, SessionKey, or Password. +func (ehb *EncryptionHandleBuilder) HiddenRecipients(hiddenRecipients *KeyRing) *EncryptionHandleBuilder { + ehb.handle.HiddenRecipients = hiddenRecipients + return ehb +} + +// SigningKey sets the signing key that are used to create signature of the message. +// Triggers that signatures are created for each signing key. +// If not set, no signature is included. +func (ehb *EncryptionHandleBuilder) SigningKey(key *Key) *EncryptionHandleBuilder { + var err error + if ehb.handle.SignKeyRing == nil { + ehb.handle.SignKeyRing, err = NewKeyRing(key) + } else { + err = ehb.handle.SignKeyRing.AddKey(key) + } + ehb.err = err + return ehb +} + +// SigningKeys sets the signing keys that are used to create signature of the message. +// Triggers that signatures are created for each signing key. +// If not set, no signature is included. +func (ehb *EncryptionHandleBuilder) SigningKeys(signingKeys *KeyRing) *EncryptionHandleBuilder { + ehb.handle.SignKeyRing = signingKeys + return ehb +} + +// SigningContext provides a signing context for the signature in the message. +// Triggers that each signature includes the sining context. +// SigningKeys have to be set if a SigningContext is provided. +func (ehb *EncryptionHandleBuilder) SigningContext(siningContext *SigningContext) *EncryptionHandleBuilder { + ehb.handle.SigningContext = siningContext + return ehb +} + +// SessionKey sets the session key the message should be encrypted with. +// Triggers session key encryption with the included session key. +// If not set, set another the type of encryption: Recipients, HiddenRecipients, or Password. +func (ehb *EncryptionHandleBuilder) SessionKey(sessionKey *SessionKey) *EncryptionHandleBuilder { + ehb.handle.SessionKey = sessionKey + return ehb +} + +// Password sets a password the message should be encrypted with. +// Triggers password based encryption with a key derived from the password. +// If not set, set another the type of encryption: Recipients, HiddenRecipients, or SessionKey. +func (ehb *EncryptionHandleBuilder) Password(password []byte) *EncryptionHandleBuilder { + ehb.handle.Password = password + return ehb +} + +// Compress indicates if the plaintext should be compressed before encryption. +// Compression affects security and opens the door for side-channel attacks, which +// might allow to extract the plaintext data without a decryption key. +// The openpgp crypto refresh recommends to not use compression. +func (ehb *EncryptionHandleBuilder) Compress() *EncryptionHandleBuilder { + ehb.handle.Compression = constants.DefaultCompression + return ehb +} + +// CompressWith indicates if the plaintext should be compressed before encryption. +// Compression affects security and opens the door for side-channel attacks, which +// might allow to extract the plaintext data without a decryption key. +// The openpgp crypto refresh recommends to not use compression. +// Allowed config options: +// constants.NoCompression: none, constants.DefaultCompression: profile default +// constants.ZIPCompression: zip, constants.ZLIBCompression: zlib. +func (ehb *EncryptionHandleBuilder) CompressWith(config int8) *EncryptionHandleBuilder { + switch config { + case constants.NoCompression, + constants.DefaultCompression, + constants.ZIPCompression, + constants.ZLIBCompression: + ehb.handle.Compression = config + } + return ehb +} + +// Utf8 indicates if the plaintext should be signed with a text type +// signature. If set, the plaintext is signed after canonicalising the line endings. +func (ehb *EncryptionHandleBuilder) Utf8() *EncryptionHandleBuilder { + ehb.handle.IsUTF8 = true + return ehb +} + +// DetachedSignature indicates that the message should be signed, +// but the signature should not be included in the same pgp message as the input data. +// Instead the detached signature is encrypted in a separate pgp message. +func (ehb *EncryptionHandleBuilder) DetachedSignature() *EncryptionHandleBuilder { + ehb.handle.DetachedSignature = true + return ehb +} + +// PlainDetachedSignature indicates that the message should be signed, +// but the signature should not be included in the same pgp message as the input data. +// Instead the detached signature is a separate signature pgp message. +// If DetachedSignature signature is set (i.e., the detached signature is encrypted), this option is ignored. +// NOTE: A plaintext detached signature might reveal information about the encrypted plaintext. Thus, use with care. +func (ehb *EncryptionHandleBuilder) PlainDetachedSignature() *EncryptionHandleBuilder { + ehb.handle.PlainDetachedSignature = true + return ehb +} + +// IncludeExternalSignature indicates that the provided signature should be included +// in the produced encrypted message. +// Special feature: should not be used in normal use-cases, +// can lead to broken or invalid PGP messages. +func (ehb *EncryptionHandleBuilder) IncludeExternalSignature(signature []byte) *EncryptionHandleBuilder { + ehb.handle.ExternalSignature = signature + return ehb +} + +// EncryptionTime allows to specify a separate time for selecting encryption keys +// instead of the internal clock (also used for signing). Note that the internal clock can be changed with SignTime. +// If the input unixTime is 0 no expiration checks are performed on the encryption keys. +func (ehb *EncryptionHandleBuilder) EncryptionTime(unixTime int64) *EncryptionHandleBuilder { + if unixTime == 0 { + ehb.handle.encryptionTimeOverride = ZeroClock() + } else { + ehb.handle.encryptionTimeOverride = NewConstantClock(unixTime) + } + return ehb +} + +// SignTime sets the internal clock to always return +// the supplied unix time for signing instead of the system time. +func (ehb *EncryptionHandleBuilder) SignTime(unixTime int64) *EncryptionHandleBuilder { + ehb.handle.clock = NewConstantClock(unixTime) + return ehb +} + +// New creates an EncryptionHandle and checks that the given +// combination of parameters is valid. If the parameters are invalid +// an error is returned. +func (ehb *EncryptionHandleBuilder) New() (PGPEncryption, error) { + if ehb.err != nil { + return nil, ehb.err + } + ehb.err = ehb.handle.validate() + if ehb.err != nil { + return nil, ehb.err + } + params := ehb.handle + ehb.handle = defaultEncryptionHandle(ehb.handle.profile, ehb.defaultClock) + return params, nil +} + +// Error returns an errors that occurred within the builder. +func (ehb *EncryptionHandleBuilder) Error() error { + return ehb.err +} diff --git a/crypto/encryption_session.go b/crypto/encryption_session.go new file mode 100644 index 00000000..0398894d --- /dev/null +++ b/crypto/encryption_session.go @@ -0,0 +1,223 @@ +package crypto + +import ( + "bytes" + "io" + "strconv" + "time" + + "github.com/ProtonMail/go-crypto/openpgp/packet" + "github.com/pkg/errors" +) + +// decryptSessionKey returns the decrypted session key from one or multiple binary encrypted session key packets. +func decryptSessionKey(keyRing *KeyRing, keyPacket []byte) (*SessionKey, error) { + var p packet.Packet + var ek *packet.EncryptedKey + + var err error + var hasPacket = false + var decryptErr error + + keyReader := bytes.NewReader(keyPacket) + packets := packet.NewReader(keyReader) + +Loop: + for { + if p, err = packets.Next(); err != nil { + break + } + + switch p := p.(type) { + case *packet.EncryptedKey: + hasPacket = true + ek = p + unverifiedEntities := keyRing.entities.EntitiesById(p.KeyId) + for _, unverifiedEntity := range unverifiedEntities { + keys := unverifiedEntity.DecryptionKeys(p.KeyId, time.Time{}, &packet.Config{}) + for _, key := range keys { + priv := key.PrivateKey + if priv.Encrypted { + continue + } + + if decryptErr = ek.Decrypt(priv, nil); decryptErr == nil { + break Loop + } + } + } + case *packet.SymmetricallyEncrypted, + *packet.AEADEncrypted, + *packet.Compressed, + *packet.LiteralData: + break Loop + + default: + continue Loop + } + } + + if !hasPacket { + if err != nil { + return nil, errors.Wrap(err, "gopenpgp: couldn't find a session key packet") + } else { + return nil, errors.New("gopenpgp: couldn't find a session key packet") + } + } + + if decryptErr != nil { + return nil, errors.Wrap(decryptErr, "gopenpgp: error in decrypting") + } + + if ek == nil || ek.Key == nil { + return nil, errors.New("gopenpgp: unable to decrypt session key: no valid decryption key") + } + + return newSessionKeyFromEncrypted(ek) +} + +// encryptSessionKey encrypts the session key with the unarmored +// publicKey and returns a binary public-key encrypted session key packet. +func encryptSessionKey( + recipients *KeyRing, + hiddenRecipients *KeyRing, + sk *SessionKey, + date time.Time, + config *packet.Config) ([]byte, error) { + outbuf := &bytes.Buffer{} + err := encryptSessionKeyToWriter(recipients, hiddenRecipients, sk, outbuf, date, config) + if err != nil { + return nil, err + } + return outbuf.Bytes(), nil +} + +// EncryptSessionKeyToWriter encrypts the session key with the unarmored +// publicKey and returns a binary public-key encrypted session key packet. +func encryptSessionKeyToWriter( + recipients *KeyRing, + hiddenRecipients *KeyRing, + sk *SessionKey, + outputWriter io.Writer, + date time.Time, + config *packet.Config, +) (err error) { + var cf packet.CipherFunction + if sk.v6 { + cf = config.Cipher() + } else { + cf, err = sk.GetCipherFunc() + } + if err != nil { + return errors.Wrap(err, "gopenpgp: unable to encrypt session key") + } + pubKeys := make([]*packet.PublicKey, 0, len(recipients.getEntities())+len(hiddenRecipients.getEntities())) + aeadSupport := config.AEAD() != nil + for _, e := range append(recipients.getEntities(), hiddenRecipients.getEntities()...) { + encryptionKey, ok := e.EncryptionKey(date, config) + if !ok { + return errors.New("gopenpgp: encryption key is unavailable for key id " + strconv.FormatUint(e.PrimaryKey.KeyId, 16)) + } + primarySelfSignature, _ := e.PrimarySelfSignature(date, config) + if primarySelfSignature == nil { + return errors.Wrap(err, "gopenpgp: entity without a self-signature") + } + + if !primarySelfSignature.SEIPDv2 { + aeadSupport = false + } + pubKeys = append(pubKeys, encryptionKey.PublicKey) + } + if len(pubKeys) == 0 { + return errors.New("gopenpgp: cannot set key: no public key available") + } + + for index, pub := range pubKeys { + isHidden := index >= len(recipients.getEntities()) + err := packet.SerializeEncryptedKeyAEADwithHiddenOption(outputWriter, pub, cf, aeadSupport, sk.Key, isHidden, nil) + if err != nil { + return errors.Wrap(err, "gopenpgp: cannot set key") + } + } + return nil +} + +// decryptSessionKeyWithPassword decrypts the binary symmetrically encrypted +// session key packet and returns the session key. +func decryptSessionKeyWithPassword(keyPacket, password []byte) (*SessionKey, error) { + keyReader := bytes.NewReader(keyPacket) + packets := packet.NewReader(keyReader) + + var symKeys []*packet.SymmetricKeyEncrypted + for { + var p packet.Packet + var err error + if p, err = packets.Next(); err != nil { + break + } + + if p, ok := p.(*packet.SymmetricKeyEncrypted); ok { + symKeys = append(symKeys, p) + } + } + + // Try the symmetric passphrase first + if len(symKeys) != 0 && password != nil { + for _, s := range symKeys { + key, cipherFunc, err := s.Decrypt(password) + if err == nil { + sk := &SessionKey{ + Key: key, + Algo: getAlgo(cipherFunc), + v6: cipherFunc == 0, // for v6 there is not algorithm specified + } + + if err = sk.checkSize(); !sk.v6 && err != nil { + return nil, errors.Wrap(err, "gopenpgp: unable to decrypt session key with password") + } + + return sk, nil + } + } + } + + return nil, errors.New("gopenpgp: unable to decrypt any packet") +} + +// encryptSessionKeyWithPassword encrypts the session key with the password and +// returns a binary symmetrically encrypted session key packet. +func encryptSessionKeyWithPassword(sk *SessionKey, password []byte, config *packet.Config) (encrypted []byte, err error) { + outbuf := &bytes.Buffer{} + err = encryptSessionKeyWithPasswordToWriter(password, sk, outbuf, config) + if err != nil { + return nil, err + } + return outbuf.Bytes(), nil +} + +func encryptSessionKeyWithPasswordToWriter(password []byte, sk *SessionKey, outputWriter io.Writer, config *packet.Config) (err error) { + var cf packet.CipherFunction + if sk.v6 { + cf = config.Cipher() + } else { + cf, err = sk.GetCipherFunc() + } + if err != nil { + return errors.Wrap(err, "gopenpgp: unable to encrypt session key with password") + } + + if len(password) == 0 { + return errors.New("gopenpgp: password can't be empty") + } + + if err = sk.checkSize(); !sk.v6 && err != nil { + return errors.Wrap(err, "gopenpgp: unable to encrypt session key with password") + } + config.DefaultCipher = cf + + err = packet.SerializeSymmetricKeyEncryptedReuseKey(outputWriter, sk.Key, password, config) + if err != nil { + return errors.Wrap(err, "gopenpgp: unable to encrypt session key with password") + } + return nil +} diff --git a/crypto/gopenpgp.go b/crypto/gopenpgp.go deleted file mode 100644 index d5ae56d5..00000000 --- a/crypto/gopenpgp.go +++ /dev/null @@ -1,26 +0,0 @@ -// Package crypto provides a high-level API for common OpenPGP functionality. -package crypto - -import "sync" - -// GopenPGP is used as a "namespace" for many of the functions in this package. -// It is a struct that keeps track of time skew between server and client. -type GopenPGP struct { - latestServerTime int64 - generationOffset int64 - lock *sync.RWMutex -} - -var pgp = GopenPGP{ - latestServerTime: 0, - generationOffset: 0, - lock: &sync.RWMutex{}, -} - -// clone returns a clone of the byte slice. Internal function used to make sure -// we don't retain a reference to external data. -func clone(input []byte) []byte { - data := make([]byte, len(input)) - copy(data, input) - return data -} diff --git a/crypto/interop_test.go b/crypto/interop_test.go new file mode 100644 index 00000000..1e7f36c3 --- /dev/null +++ b/crypto/interop_test.go @@ -0,0 +1,569 @@ +package crypto + +import ( + "bytes" + "testing" + + "github.com/ProtonMail/gopenpgp/v3/internal" +) + +func TestDetachedSignaturesWithUnknownPackets(t *testing.T) { + const ricarda_key_armor = `-----BEGIN PGP PUBLIC KEY BLOCK----- +Comment: 2ADE 0F8A A059 6BC9 4E50 D2AD 9162 53AB 652E F195 + +xsDNBGJvrDABDACrZUY7VU1uZ/uzntlAKEHVF4mb3eYSdW1rE3hVke0HoDvtQbzq +KQ9qgfPaNwdkxRexgrNGSeKkQJcgR7gMWxFxM/FwddQIXfVL43nRlN+iGvFDYR+9 +dn5gSOD9EvZUYLN9p6yR3Umyglt4NpdjYM0J+Rn2DVyfGHCtS+z1fdym1h1zdImo +rArBpWMEdvNN/6dR8BN67WSBs5pVsvnDPdjbeU+GPJVoRH4CWe/LdJnJDICPmlva +gAyeJeK+KitkxD8IIc9d18x5dV1Z/LL2o1B0Psort8+az2Z+NbkP2cUv0DDRyZIM +Ww1A6KMSSNuvewXGCU+gEFlGIA83zfq7XE3WNp9EPy9xCs610+KxSS9cftKdrMmU +cl37Gb1lZyLFIBUCwPdIyTvhs6r6rMgu4GQ46sPl8qeLbtaoXnPWj9V1Fn9RoMgz +Kq8ZItFCJfCbf+gamx/0S/0mE47giuAtymw8Hf53e3jsSANK28GpcsEUNqWT4djb +YgcWl2iXR7n0i9cAEQEAAcLBSQQfAQoAfQWCYm+sMAMLCQcJEJFiU6tlLvGVRxQA +AAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZ6O6vv9v4H1aAR9Z +aEksrL/2rQGm4Omv2SCubCC1pk/DAxUKCAKbAQIeARYhBCreD4qgWWvJTlDSrZFi +U6tlLvGVAACp1wv+MEkIs8x2Wk6e+i4rbclnkBjR2tSwv3N66jcz09QeDt5xwX1Q +jq1TP+XGFJfFA+LUugbLWNzWqwh7CypSc6IAB82+Ha+lCMjY3SZLfTe6crUgDWOZ +wDprVND6z2g9UlgJ6rfFf329LEUziknjKFBbreONH12tOzvCxrxipp84J0DZEqO3 +0VB2b2B7uO1X5V1aQMFj4JEafQ6Or6uXtR3Fwcs/JALwhqydy3LI4Y0s/y91qdPP +69DgugVDbG/Kkp5W2CNtGyd51cnEoFEbh9nV0hHR+rERU2ZcyoNfZQCyPuHMIT1L +PndcWLsT6Ga3TvXLUEhOOl6CPpox0aV7bsGpip0u54yGkhgR5YYLchcTVt15q5+/ +M5Z03et1LcSZKk1ijSFXNsOWdD2ojneN3vhlKbJoDosT10WyCsXKL8JNP4F4q4nU +22+/N1sttbgzSatwPxyiC7kKymQmDtg8NCyyCtpDoRkEIzaMv+4DqoMVkvLRcF1K +vX+rNEg1OxneqMSEzS1SaWNhcmRhIFMuIMOBbHZhcmV6IDxyaWNhcmRhQG9wZW5w +Z3AuZXhhbXBsZT7CwUwEEwEKAIAFgmJvrDADCwkHCRCRYlOrZS7xlUcUAAAAAAAe +ACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmfo22n7d9n+Nv3PxzMP93f7 +rejDV4BFsobH6T5cZGvj8AMVCggCmQECmwECHgEWIQQq3g+KoFlryU5Q0q2RYlOr +ZS7xlQAAu6EL/iJ91h1dwcQMZpbKYnBbH/1U/ncbIlhkQf8YzjrpzVprU0sr4cKu +mx42EylcteODVoDN8ITGROUC8Prw5tu+NS2mjkKWUlj56eLkpS3A3D6NqPIFEzU3 +KYztp+Y0hq2ZmsGsPgL1AbVPc2+ngUSL4pXgQ5hmVoD3B6VGqBReyKXhZNzH42S5 +fMuOsY8+/xgQ0WDwSCVeUW7Otql24iFji9U4eL036AzOO1hXkbOesrw4E7GmU2mR +kkX/aDZ9bGYT3yVZ/CJkU4wrUHJWW1IJl/bl//becL8Vnqr92vQkYQnUh19zIi2n +6ualROjcoITjjRemvkBfM9zXTd6kVFKD+ySDnGtNq62Ukz0/COg81tAnnnXeNhlP +1M8yA6zfrY17tAFYLmALUrPYjVy4ZJuaHScnIcM5lHIKYn1ijetGQSjI4sT/mSi4 +8fwNHFRufR7ta9XWv5b4+m8SAyrJ/FqnRoOwtoph0ZxSItjpv+qLPX2N4M8/JOqh +3QhI5ZDd+ScGJc7AzQRib6wwAQwArMv2IdO1f8a/brBc/+LeCqyqR+qLKzZRLAvF ++c6+qG7D9OzZCJfbPltnQ5BgRzIiSKgzDugDDt0m3fdWBxOQG5Ojo53Xu1ZTgRUD +0KWI4kH5Cs0gp3Kl62TdNAxNUlHdWqJ6G6WYVMlMhLmBVPjo65pT1+OON2v/O/qA +8ZJRlK3RyKym4Kcr8JbtX1roSRtVPRpi++Y1CGuhnK3UM68putqfgZOnTZxln2m2 +BUrL8fWgme2rTJyrrN8fSM61dZZ+5E0SWDMBxEp5bimMrRjJc7V0JMUx2HiCDe/l +panzleAugfEVUeAYsEe9C+7k3p2r00roL0dWnrMgfhGnJy5ccmPyzEQpoA6ARq1a +EBk2Syr/QNO93GfDFcK9pTcDcQdUgq93x5s9LShLQ7fuvVXXxkl4QkdKJn+Vb2yP +e2Bw6VmMy40JUVyMvUY99YUK+y8iZmA+ro6naeJn+P/uCiURL+dMoWmlGsCbZjz0 +sVh1t5pH+gPRI531U7BfNX6OmRk9ABEBAAHCwT4EGAEKAHIFgmJvrDAJEJFiU6tl +LvGVRxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZ+GZb8lQ +10FTIysD5BKY6IJRAGxevRcudbWb9ItkymXvApsMFiEEKt4PiqBZa8lOUNKtkWJT +q2Uu8ZUAAFl5C/wLIh2UcurP0mpNaaSiRGgNVrFCPceF4rFlY/1/yJbNE8yWIEQt +rI4Dh2jdP7mxlfSH8SMsAPkSl9mA8xIGXHUiWkN+tEh4v3BurccaSUMA81+FveC3 +jSR9AtXECk/Bk6l4gAz2qRRwq9uErxZD+IuZN/W6uue6z4nnSET7qc9q7vu6tDYR +g8J6vXef4RdIq7pRsdSMSNFTIHSDEgXpGV+ru+7Y98ipHhwKqYHUlMgTX56m8HQ3 +1uJvgFFGkKVwQQORiu48mgqdAFHgHrree32BxDpxAJstvsdGcvNraaFqAgkikFHV +DiScG1yKZnYgzeJhI6eNwxpDDNl5FkHub8YOXftr936Is4jmKVg7H430502dh5ko +A695dMCpCo8uaoGduWx/7Mh+SmV9WbqjHQlWKZbnQ0eCAyn2TZD7VyI1/QCz8dsb +YC5wLe0xqQxx0fEPqHZS57QerBlKQ8eaxEIUpTx63LrPvu15XsBMCsGPik4gTpqR +ZTWc+9wHu+IMtobOwM0EYm+sMAEMALpkWQP4+YULtR+qeJX0OlJkWIk77o5/7TwZ +n/Ho+fz9hnXmp6YtrSUFIofJ9LpcKWyx8OB9G/8FBG1TZXHdgndAkKpdzN9fVKay +86+p9+F3ExoioFazjYiXNJFwgtIFcHXkibnOUJvtftvJlFoupQBPAih89Jlg1OBt +80wRW44zC6IBxWWfMIKVuMPIOw6sMxKh8vz2bBfa8S3N9Mxi8t2ncKQuTi4Hy4Hr +o6duFuUBUVTSqzrxvq3uS+CrWSae4xpjgSOf/gWjKfRqWip5fT/DtKkmarQ7dqLP +C8IncBpLo4riREDu3lub/AFjDLyvZ1rg/F4CeNYK6xsdgcZvF00+a4nKZaW4KAS/ +MDBHYaK0WFbMebtHO2veCQ/+DUqyaRf7Trrr56h11ffGGDuGR3m9XLXaQJ07Ct87 +Kpywly5ZaSRs8vRtyrxCfmAX7QWnjio/FeI2JzJdyMlGfzDV+VjCBUVXin8DlJ8v +MV8n9/Uzu6LSMOJKbVX8VZSg28YWuwARAQABwsM8BBgBCgJwBYJib6wwCRCRYlOr +ZS7xlUcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmcHvNZ+ +NAzY3Y0vZZy2BB5vTzG2mB28DjINb6OcDwAWbgKbAsE8oAQZAQoAbwWCYm+sMAkQ +s/nxXZcRZK5HFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3Jn +tYbTPBH4zs1yFsUSK50UToSSLpDbHSCIVT0CZFQSOywWIQTfVkurgqxmNp++Bc+z ++fFdlxFkrgAAJOsL/0in5Hj6I+/bDavsLhg/atB4UUP3EBO6x0rW5TUuW3UxYrlT +yjza4aVYPHbcm7DNmkHPoYoU4l374kOozn7cXX2hB1xOMINd8MI/cfoKD9oL3hGF +BdhuVgyJZAUVfQKIvoAcaYUjivRCIbrUkgIkqFSYTPwJ792mrkQXecRdHLbP/OcP +tBgLB+lfnFdNh0KU5HIN5E/Ohse3it+HyRUAcNdkYH/VxTYTOTXYUt8kO7Rpe6uI +YcfPzPnXqGub6lbF4pXvQQRyuj/lPOPcPtBrpZgZFCXu0nl8EIJRdAZOb2eclBft +rrYf7z/jwi/z9rPNvDMyuKotgrmppiYdgraTNh9v6cLRQiKSjit5sK4FsJeXiP21 +xbwb22j5fJyZqksbgq1zBGarmbdIbJ07oGHkaVFkO2/rXoseWaUKkQM0VDw9aDa/ +Qe0vuMiHa5B04HkzxvJdI3XcJ9vLpqCKNoFbksGlSuc5N6euAYRjMFbMaPl2f+k5 +4xny8TGYmncul26DSBYhBCreD4qgWWvJTlDSrZFiU6tlLvGVAAC/VgwAnczy13qB +4bVGkcjGGGjw7coqUZwVihwXqf8uhh9iSJcTocslYYnoB/K/4nHab6Xor92lCRJO +iw2LByr+YhzRwkxog//PvvAjAvGCoidpIfU8FMkUd4X57e95MvOpD/ePojOVmFCE +gW/77VDIZcpJ3VYNx+VBv6FxnbnXO3Kd62p7SKiHOnAZgH/U+tq8qIFUv0QLJtzG +4BbppPTjIH+gZc2nrXp0sZ2Ov296qwl62ZprW2S17ljFTmQv6zcnicC0J25F5Zro +GWgMAJcpdb77V3PlPCy4QhttrMjGpGPtzVGjm13YxaEkGuPCvWmoM/6nqAVOZFmy +zTgQjuQpJElwQGI+pi8DvzkKmLo5N7+0iOjBF4hTLKQ6AP7+9WxLCgHDtjRgFPtp +Vo1sVqH0l596O4qSgs81JLtS7SwXfihyRAxXqrlvseAEaPNjK/FgQym3q17UqElm +u1Fr1U9mB1v6s2rXN2USut8xQECpA+OIiUItczMBd/OYOnu0Y3eImkYc +=i+jK +-----END PGP PUBLIC KEY BLOCK----- +` + const sig = `-----BEGIN PGP SIGNATURE----- + +wsE7BAABCgBvBYJkit2rCRD7/MgqAV5zMEcUAAAAAAAeACBzYWx0QG5vdGF0aW9u +cy5zZXF1b2lhLXBncC5vcmelgsnbOPsc1mt9YRUNBmoAWxgPfqyzTjfUu2WQ1NQd +MhYhBNGmbhojsYLJmA94jPv8yCoBXnMwAAC0TAv8CsSeavcLIjJvUKBMV+I09fmA +D/HCf9ZtV7Y+JPOaOMBgWnNaoVINRenDbQLz69Yq8Xf8zyOOdX1GUpiO/0vdHnw2 +TdJc6tkWB2yP63DfwGQD5xxSzltfvO1GV7BUYVIUXZqATp39wp7nmn6BJ15DtlLT +mUwwQ0eDfwtAGcyZBfDkz1V7p+AYuWWDCTRXk0LVl8mjsXheX1BX4uzlru6LWE88 +JL1uNDcRfvg1WDBCV0zTQwWlmmsWF42KCft6p/c+ak5XstzEKalnDtASoU/o07bF +QXVr2ncHexfjyyZRo9ZH0s4jn71J83Djou4O6O9zgo19kL1aErEDC3J9n041qvvV +xi5ZVvmVVeTrGrJ71UyoMCRxyFu+vtBnvoRluLcw5NxKgniASJCRtXvUYYxG048I +9m6pSkC41QlEc+4r325i/Ny4p6VLwZDK+APN1MQRd85MHjKpGNAiVHYLtdAzdABr +VpAFLmdML2VLIiCG9R6Zw8U2Hww5DSy5Gq0UM1tJwsE7BAABCgBvBYJkit2rCRCz ++fFdlxFkrkcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmdI +0DufwhTqCzYcRYQp4VAE+Qxp2Ruj+srVlIBkA20qZRYhBN9WS6uCrGY2n74Fz7P5 +8V2XEWSuAADSCwwAhfAVzNHeLEgrXHJAgrpP31ubBebEVmOl3jUPg4+cko2LJBGE +egeONp4kVrxMmk9AKURfQ4UaVFXh6Cyhjq5zFZQJnZ2fmHJm/L1A7r+XUwTQ2dZy +LGVSZCPNbKEuJxcZzYO9fDtbJIoY5OeIBFmasvbGle4laotKhleMopfqzJLuZJIa +zxDAsJbdcm/sVbHLeElK4cjcEtiZZKNizzZnTE827WlZVll1FZ+a3Gi8SQXV+asy +aRVUb1hw1VzFj3ZmtcRIPctEy5uYZq8Oaa0U8Z+J10ertSGh/2I1Au7G7SXiYVzB +65LXj4daJ3JZTCjpqc82AJdnpVpREPpCfaopUchA+NYNaIaolx/SmgkeLxyWXff+ +5GHfuVDQpD8Ma7hOx6aP+Ih/xI8OrMgx9sZ79L9mdbcSCaypqYd/oez1pwrfvcN8 +c6rmTo+LPGN1CKceOyPdcKEZBnwY/QrNf3GDgZSnm69Iol16OvqstTrFU26z1WT6 +sWC04HotaaBXdIHf +=nNOz +-----END PGP SIGNATURE-----` + message := "Hello World :)" + + key, err := NewKeyFromArmored(ricarda_key_armor) + if err != nil { + t.Error("error: ", err) + } + pgp := PGP() + verifier, err := pgp.Verify().VerificationKey(key).New() + if err != nil { + t.Error("error: ", err) + } + res, err := verifier.VerifyDetached([]byte(message), []byte(sig), Armor) + if err != nil { + t.Error("error: ", err) + } + if err = res.SignatureError(); err != nil { + t.Error("Should have no signature error, got:", err) + } +} + +func TestSignedAndEncryptedMessages(t *testing.T) { + const bob_key_armor = `-----BEGIN PGP PRIVATE KEY BLOCK----- +Comment: Bob's OpenPGP Transferable Secret Key + +lQVYBF2lnPIBDAC5cL9PQoQLTMuhjbYvb4Ncuuo0bfmgPRFywX53jPhoFf4Zg6mv +/seOXpgecTdOcVttfzC8ycIKrt3aQTiwOG/ctaR4Bk/t6ayNFfdUNxHWk4WCKzdz +/56fW2O0F23qIRd8UUJp5IIlN4RDdRCtdhVQIAuzvp2oVy/LaS2kxQoKvph/5pQ/ +5whqsyroEWDJoSV0yOb25B/iwk/pLUFoyhDG9bj0kIzDxrEqW+7Ba8nocQlecMF3 +X5KMN5kp2zraLv9dlBBpWW43XktjcCZgMy20SouraVma8Je/ECwUWYUiAZxLIlMv +9CurEOtxUw6N3RdOtLmYZS9uEnn5y1UkF88o8Nku890uk6BrewFzJyLAx5wRZ4F0 +qV/yq36UWQ0JB/AUGhHVPdFf6pl6eaxBwT5GXvbBUibtf8YI2og5RsgTWtXfU7eb +SGXrl5ZMpbA6mbfhd0R8aPxWfmDWiIOhBufhMCvUHh1sApMKVZnvIff9/0Dca3wb +vLIwa3T4CyshfT0AEQEAAQAL/RZqbJW2IqQDCnJi4Ozm++gPqBPiX1RhTWSjwxfM +cJKUZfzLj414rMKm6Jh1cwwGY9jekROhB9WmwaaKT8HtcIgrZNAlYzANGRCM4TLK +3VskxfSwKKna8l+s+mZglqbAjUg3wmFuf9Tj2xcUZYmyRm1DEmcN2ZzpvRtHgX7z +Wn1mAKUlSDJZSQks0zjuMNbupcpyJokdlkUg2+wBznBOTKzgMxVNC9b2g5/tMPUs +hGGWmF1UH+7AHMTaS6dlmr2ZBIyogdnfUqdNg5sZwsxSNrbglKP4sqe7X61uEAIQ +bD7rT3LonLbhkrj3I8wilUD8usIwt5IecoHhd9HziqZjRCc1BUBkboUEoyedbDV4 +i4qfsFZ6CEWoLuD5pW7dEp0M+WeuHXO164Rc+LnH6i1VQrpb1Okl4qO6ejIpIjBI +1t3GshtUu/mwGBBxs60KBX5g77mFQ9lLCRj8lSYqOsHRKBhUp4qM869VA+fD0BRP +fqPT0I9IH4Oa/A3jYJcg622GwQYA1LhnP208Waf6PkQSJ6kyr8ymY1yVh9VBE/g6 +fRDYA+pkqKnw9wfH2Qho3ysAA+OmVOX8Hldg+Pc0Zs0e5pCavb0En8iFLvTA0Q2E +LR5rLue9uD7aFuKFU/VdcddY9Ww/vo4k5p/tVGp7F8RYCFn9rSjIWbfvvZi1q5Tx ++akoZbga+4qQ4WYzB/obdX6SCmi6BndcQ1QdjCCQU6gpYx0MddVERbIp9+2SXDyL +hpxjSyz+RGsZi/9UAshT4txP4+MZBgDfK3ZqtW+h2/eMRxkANqOJpxSjMyLO/FXN +WxzTDYeWtHNYiAlOwlQZEPOydZFty9IVzzNFQCIUCGjQ/nNyhw7adSgUk3+BXEx/ +MyJPYY0BYuhLxLYcrfQ9nrhaVKxRJj25SVHj2ASsiwGJRZW4CC3uw40OYxfKEvNC +mer/VxM3kg8qqGf9KUzJ1dVdAvjyx2Hz6jY2qWCyRQ6IMjWHyd43C4r3jxooYKUC +YnstRQyb/gCSKahveSEjo07CiXMr88UGALwzEr3npFAsPW3osGaFLj49y1oRe11E +he9gCHFm+fuzbXrWmdPjYU5/ZdqdojzDqfu4ThfnipknpVUM1o6MQqkjM896FHm8 +zbKVFSMhEP6DPHSCexMFrrSgN03PdwHTO6iBaIBBFqmGY01tmJ03SxvSpiBPON9P +NVvy/6UZFedTq8A07OUAxO62YUSNtT5pmK2vzs3SAZJmbFbMh+NN204TRI72GlqT +t5hcfkuv8hrmwPS/ZR6q312mKQ6w/1pqO9qitCFCb2IgQmFiYmFnZSA8Ym9iQG9w +ZW5wZ3AuZXhhbXBsZT6JAc4EEwEKADgCGwMFCwkIBwIGFQoJCAsCBBYCAwECHgEC +F4AWIQTRpm4aI7GCyZgPeIz7/MgqAV5zMAUCXaWe+gAKCRD7/MgqAV5zMG9sC/9U +2T3RrqEbw533FPNfEflhEVRIZ8gDXKM8hU6cqqEzCmzZT6xYTe6sv4y+PJBGXJFX +yhj0g6FDkSyboM5litOcTupURObVqMgA/Y4UKERznm4fzzH9qek85c4ljtLyNufe +doL2pp3vkGtn7eD0QFRaLLmnxPKQ/TlZKdLE1G3u8Uot8QHicaR6GnAdc5UXQJE3 +BiV7jZuDyWmZ1cUNwJkKL6oRtp+ZNDOQCrLNLecKHcgCqrpjSQG5oouba1I1Q6Vl +sP44dhA1nkmLHtxlTOzpeHj4jnk1FaXmyasurrrI5CgU/L2Oi39DGKTH/A/cywDN +4ZplIQ9zR8enkbXquUZvFDe+Xz+6xRXtb5MwQyWODB3nHw85HocLwRoIN9WdQEI+ +L8a/56AuOwhs8llkSuiITjR7r9SgKJC2WlAHl7E8lhJ3VDW3ELC56KH308d6mwOG +ZRAqIAKzM1T5FGjMBhq7ZV0eqdEntBh3EcOIfj2M8rg1MzJv+0mHZOIjByawikad +BVgEXaWc8gEMANYwv1xsYyunXYK0X1vY/rP1NNPvhLyLIE7NpK90YNBj+xS1ldGD +bUdZqZeef2xJe8gMQg05DoD1DF3GipZ0Ies65beh+d5hegb7N4pzh0LzrBrVNHar +29b5ExdI7i4iYD5TO6Vr/qTUOiAN/byqELEzAb+L+b2DVz/RoCm4PIp1DU9ewcc2 +WB38Ofqut3nLYA5tqJ9XvAiEQme+qAVcM3ZFcaMt4I4dXhDZZNg+D9LiTWcxdUPB +leu8iwDRjAgyAhPzpFp+nWoqWA81uIiULWD1Fj+IVoY3ZvgivoYOiEFBJ9lbb4te +g9m5UT/AaVDTWuHzbspVlbiVe+qyB77C2daWzNyx6UYBPLOo4r0t0c91kbNE5lgj +Z7xz6los0N1U8vq91EFSeQJoSQ62XWavYmlCLmdNT6BNfgh4icLsT7Vr1QMX9jzn +JtTPxdXytSdHvpSpULsqJ016l0dtmONcK3z9mj5N5z0k1tg1AH970TGYOe2aUcSx +IRDMXDOPyzEfjwARAQABAAv9F2CwsjS+Sjh1M1vegJbZjei4gF1HHpEM0K0PSXsp +SfVvpR4AoSJ4He6CXSMWg0ot8XKtDuZoV9jnJaES5UL9pMAD7JwIOqZm/DYVJM5h +OASCh1c356/wSbFbzRHPtUdZO9Q30WFNJM5pHbCJPjtNoRmRGkf71RxtvHBzy7np +Ga+W6U/NVKHw0i0CYwMI0YlKDakYW3Pm+QL+gHZFvngGweTod0f9l2VLLAmeQR/c ++EZs7lNumhuZ8mXcwhUc9JQIhOkpO+wreDysEFkAcsKbkQP3UDUsA1gFx9pbMzT0 +tr1oZq2a4QBtxShHzP/ph7KLpN+6qtjks3xB/yjTgaGmtrwM8tSe0wD1RwXS+/1o +BHpXTnQ7TfeOGUAu4KCoOQLv6ELpKWbRBLWuiPwMdbGpvVFALO8+kvKAg9/r+/ny +zM2GQHY+J3Jh5JxPiJnHfXNZjIKLbFbIPdSKNyJBuazXW8xIa//mEHMI5OcvsZBK +clAIp7LXzjEjKXIwHwDcTn9pBgDpdOKTHOtJ3JUKx0rWVsDH6wq6iKV/FTVSY5jl +zN+puOEsskF1Lfxn9JsJihAVO3yNsp6RvkKtyNlFazaCVKtDAmkjoh60XNxcNRqr +gCnwdpbgdHP6v/hvZY54ZaJjz6L2e8unNEkYLxDt8cmAyGPgH2XgL7giHIp9jrsQ +aS381gnYwNX6wE1aEikgtY91nqJjwPlibF9avSyYQoMtEqM/1UjTjB2KdD/MitK5 +fP0VpvuXpNYZedmyq4UOMwdkiNMGAOrfmOeT0olgLrTMT5H97Cn3Yxbk13uXHNu/ +ZUZZNe8s+QtuLfUlKAJtLEUutN33TlWQY522FV0m17S+b80xJib3yZVJteVurrh5 +HSWHAM+zghQAvCesg5CLXa2dNMkTCmZKgCBvfDLZuZbjFwnwCI6u/NhOY9egKuUf +SA/je/RXaT8m5VxLYMxwqQXKApzD87fv0tLPlVIEvjEsaf992tFEFSNPcG1l/jpd +5AVXw6kKuf85UkJtYR1x2MkQDrqY1QX/XMw00kt8y9kMZUre19aCArcmor+hDhRJ +E3Gt4QJrD9z/bICESw4b4z2DbgD/Xz9IXsA/r9cKiM1h5QMtXvuhyfVeM01enhxM +GbOH3gjqqGNKysx0UODGEwr6AV9hAd8RWXMchJLaExK9J5SRawSg671ObAU24SdY +vMQ9Z4kAQ2+1ReUZzf3ogSMRZtMT+d18gT6L90/y+APZIaoArLPhebIAGq39HLmJ +26x3z0WAgrpA1kNsjXEXkoiZGPLKIGoe3hqJAbYEGAEKACAWIQTRpm4aI7GCyZgP +eIz7/MgqAV5zMAUCXaWc8gIbDAAKCRD7/MgqAV5zMOn/C/9ugt+HZIwX308zI+QX +c5vDLReuzmJ3ieE0DMO/uNSC+K1XEioSIZP91HeZJ2kbT9nn9fuReuoff0T0Dief +rbwcIQQHFFkrqSp1K3VWmUGp2JrUsXFVdjy/fkBIjTd7c5boWljv/6wAsSfiv2V0 +JSM8EFU6TYXxswGjFVfc6X97tJNeIrXL+mpSmPPqy2bztcCCHkWS5lNLWQw+R7Vg +71Fe6yBSNVrqC2/imYG2J9zlowjx1XU63Wdgqp2Wxt0l8OmsB/W80S1fRF5G4SDH +s9HXglXXqPsBRZJYfP+VStm9L5P/sKjCcX6WtZR7yS6G8zj/X767MLK/djANvpPd +NVniEke6hM3CNBXYPAMhQBMWhCulcoz+0lxi8L34rMN+Dsbma96psdUrn7uLaB91 +6we0CTfF8qqm7BsVAgalon/UUiuMY80U3ueoj3okiSTiHIjD/YtpXSPioC8nMng7 +xqAY9Bwizt4FWgXuLm1a4+So4V9j1TRCXd12Uc2l2RNmgDE= +=miES +-----END PGP PRIVATE KEY BLOCK----- +` + msg := `-----BEGIN PGP MESSAGE----- +Comment: Plaintext is "Hello\r\nWorld!\n" + +wcDMA3wvqk35PDeyAQv+O4Ca5MVgGytdY8tH6BEDlUlogV/GKi7cELyfOhlE5EN7 +8Aa85m5ri9Y6DAefD1QlkfU2wpu2AiWNLXcUTRdlzFFlQTs0Yb4HFfRtM5fMYAG2 +49Ut13YiDCWCe8ROohF8+Oyi55a7H6LxzqHwiAD4jtqNVqxbbzrsOVZJfsSQ9Aej +b9Th2nM3Ca/w4DPkY+CY5AimaCZlKK0JRe8NCpMBO4+j1ZulnGC+O+gmwaPcBx7u +moQ2Cl9OUVYzEvAt9h/ffJOumdWrHWKG9ZQMchwIsB8FhiIKlCegQQDLH6A12K14 +FDp/FBvXlKwrxlNE1FMH4j8af9spCreX4+N8JMgwhbrZeOdietSi2p3ozfiAGg+I +pMc8fXi4TRVLDr62r9udqYqMjEF+UAa7N68v8/VsrGCqfYzhyZI4STLEAcZS1cvS +ABqod9yd1/Cj4rbagLN1yNiGbYOoFODulgMZjBY17xQiO7/3VTgPvDuQktw68iKg +MLfnhx6r68l1qzwXR6fL0sOZAbubfGHksPIFX3BsiOFbpGzyfhjcXuUGoNSdeZwj +y1VouVT2GsMe5ARA22aaTs73MKUGSbMLIBzVcYHLdZt1TE/k/PQUtbEnpnLjXE3N +EZ8urDNkgNFpGJ+LTIOjkkOwd7mzjaKHUVHc7z9HXWMPqTZNrUzxiBpQVJLBlG97 +7Lb61LeDGDl+p9zd9nuE4/ur8E0D6stGzWtHE9f/gwW+bWmGx4Civ15q1YbFTnlJ +dsKX5ujo9o92cRGuPsyEQIWXEcadlXyHvnPdsRqiw/FSV/sPCdx/ANUo27AMQAKy +JbTUSeFiM45EzopZesVeADgixCGeHJYX1c3MAFEr4dNhF/SBHV3IVSfJXktP6FQP +heVE07E08ap7l7B+NnQsbW9SEkrZyH1Mr+KbprzD0IoOtWmGzKM9XNcqeFhsmICz +ajbrhyoRz4T3+cKiOrxlLoVu5hMklpAdZxbmbBkTxCNkYAliljiwr/92CdoAgAPL +egL75mmMjuBUus+mAlQ70LD3IxNAU1cDQxs0/AlYRqQY8I5BL08POSWSAx26n34u +yJiJPUzjFHHVXEJTnczWews/9qRxNAG7dPF4lto0kk6gLPjeOYhqqF6/1t646kZD +yHFgyhMWJUJpoGkZyCRzAmrE5Nlskhb8wElD1LrRbclpOfMFqONCxmWxThISWLgR +Q+Eowm97I6tyXajaclYQyDtaQvCFbuEM5V3Fh7yXHiMmi/n0lKiPqvYgZhJJc0ZO +M2Cdgvfac7fOzexKGfBjYIfWziPyi4lPGPKXVvZs/jW7otQeNeZiZ3/E3kUDe4zK +Q6WLle0cQMwae4XM9IJ9BNl4pGtOZnbBkwXRpt3vj0Mgh+9A3ZuLuF1UMA7KtmXr +76KbzREI24372tz3GdbI/M+m4t2xqTH5FeyrGj5hYTNeBtxqR1At6Sq2O/IMFW8U +ADVfIGW15r/KQm7+ATePm7JHrrBGg7T0AenEePrWUhgYE6y1lpctc6+tQ2UTRxSS +ac+l9jTUyYsZEXzRtxpxRbp9M8b5jn06XaD3mSIo8oFWSLN+rcl25ykkQdoHIQBR +wdeKtEK5d0QUnHXlbmyELjcqOjbd+FiGYL5t16MB7DUq9sr7CvEUYKj6uNgZm1xO ++hm95dDoNsfDdJy8d4nroTKGCjmDEzNtJe1/S6Idqy1IPk2KBalRajKtkvAUazLw +P1tdyL2lnkB0AaGKn0/ZcBn/Cuf0DFB0xuFHb+ineQAyg2l+TNl50S33vT5hYITG +BENKv7wMTLL6yEYQWKAE/43qiWoa8rtjOYnMmVjQqUqcNfB52xbBIIgHK7kE004i +QPApT/QEpOMLbL0//9HEpr3W5hT+xZb13n5svW80lExwCqr1apndeOseV7LMS+K4 +QCHl+sIx0jO21r+d84Kagez4Z9pG7YGnG6I/1TKup4RmC0Fq9Ue2iG67c2j2qU66 +Ik4fzHKnqkcmTwUng+v5sDcU2R2x4Rel6YuR +=knHG +-----END PGP MESSAGE----- +` + expected := "Hello\r\nWorld!\n" + + key, err := NewKeyFromArmored(bob_key_armor) + if err != nil { + t.Error("error: ", err) + } + pgp := PGP() + decryptor, err := pgp.Decryption().DecryptionKey(key).VerificationKey(key).New() + if err != nil { + t.Error("error: ", err) + } + res, err := decryptor.Decrypt([]byte(msg), Armor) + if err != nil { + t.Error("error: ", err) + } + if err = res.SignatureError(); err != nil { + t.Error("Should have no signature error, got:", err) + } + if !bytes.Equal(res.Bytes(), []byte(expected)) { + t.Errorf("Should be equal, got: %s", res.Bytes()) + } +} + +func TestProblematicCleartextSignature(t *testing.T) { + const bob_key_armor = `-----BEGIN PGP PRIVATE KEY BLOCK----- +Comment: Bob's OpenPGP Transferable Secret Key + +lQVYBF2lnPIBDAC5cL9PQoQLTMuhjbYvb4Ncuuo0bfmgPRFywX53jPhoFf4Zg6mv +/seOXpgecTdOcVttfzC8ycIKrt3aQTiwOG/ctaR4Bk/t6ayNFfdUNxHWk4WCKzdz +/56fW2O0F23qIRd8UUJp5IIlN4RDdRCtdhVQIAuzvp2oVy/LaS2kxQoKvph/5pQ/ +5whqsyroEWDJoSV0yOb25B/iwk/pLUFoyhDG9bj0kIzDxrEqW+7Ba8nocQlecMF3 +X5KMN5kp2zraLv9dlBBpWW43XktjcCZgMy20SouraVma8Je/ECwUWYUiAZxLIlMv +9CurEOtxUw6N3RdOtLmYZS9uEnn5y1UkF88o8Nku890uk6BrewFzJyLAx5wRZ4F0 +qV/yq36UWQ0JB/AUGhHVPdFf6pl6eaxBwT5GXvbBUibtf8YI2og5RsgTWtXfU7eb +SGXrl5ZMpbA6mbfhd0R8aPxWfmDWiIOhBufhMCvUHh1sApMKVZnvIff9/0Dca3wb +vLIwa3T4CyshfT0AEQEAAQAL/RZqbJW2IqQDCnJi4Ozm++gPqBPiX1RhTWSjwxfM +cJKUZfzLj414rMKm6Jh1cwwGY9jekROhB9WmwaaKT8HtcIgrZNAlYzANGRCM4TLK +3VskxfSwKKna8l+s+mZglqbAjUg3wmFuf9Tj2xcUZYmyRm1DEmcN2ZzpvRtHgX7z +Wn1mAKUlSDJZSQks0zjuMNbupcpyJokdlkUg2+wBznBOTKzgMxVNC9b2g5/tMPUs +hGGWmF1UH+7AHMTaS6dlmr2ZBIyogdnfUqdNg5sZwsxSNrbglKP4sqe7X61uEAIQ +bD7rT3LonLbhkrj3I8wilUD8usIwt5IecoHhd9HziqZjRCc1BUBkboUEoyedbDV4 +i4qfsFZ6CEWoLuD5pW7dEp0M+WeuHXO164Rc+LnH6i1VQrpb1Okl4qO6ejIpIjBI +1t3GshtUu/mwGBBxs60KBX5g77mFQ9lLCRj8lSYqOsHRKBhUp4qM869VA+fD0BRP +fqPT0I9IH4Oa/A3jYJcg622GwQYA1LhnP208Waf6PkQSJ6kyr8ymY1yVh9VBE/g6 +fRDYA+pkqKnw9wfH2Qho3ysAA+OmVOX8Hldg+Pc0Zs0e5pCavb0En8iFLvTA0Q2E +LR5rLue9uD7aFuKFU/VdcddY9Ww/vo4k5p/tVGp7F8RYCFn9rSjIWbfvvZi1q5Tx ++akoZbga+4qQ4WYzB/obdX6SCmi6BndcQ1QdjCCQU6gpYx0MddVERbIp9+2SXDyL +hpxjSyz+RGsZi/9UAshT4txP4+MZBgDfK3ZqtW+h2/eMRxkANqOJpxSjMyLO/FXN +WxzTDYeWtHNYiAlOwlQZEPOydZFty9IVzzNFQCIUCGjQ/nNyhw7adSgUk3+BXEx/ +MyJPYY0BYuhLxLYcrfQ9nrhaVKxRJj25SVHj2ASsiwGJRZW4CC3uw40OYxfKEvNC +mer/VxM3kg8qqGf9KUzJ1dVdAvjyx2Hz6jY2qWCyRQ6IMjWHyd43C4r3jxooYKUC +YnstRQyb/gCSKahveSEjo07CiXMr88UGALwzEr3npFAsPW3osGaFLj49y1oRe11E +he9gCHFm+fuzbXrWmdPjYU5/ZdqdojzDqfu4ThfnipknpVUM1o6MQqkjM896FHm8 +zbKVFSMhEP6DPHSCexMFrrSgN03PdwHTO6iBaIBBFqmGY01tmJ03SxvSpiBPON9P +NVvy/6UZFedTq8A07OUAxO62YUSNtT5pmK2vzs3SAZJmbFbMh+NN204TRI72GlqT +t5hcfkuv8hrmwPS/ZR6q312mKQ6w/1pqO9qitCFCb2IgQmFiYmFnZSA8Ym9iQG9w +ZW5wZ3AuZXhhbXBsZT6JAc4EEwEKADgCGwMFCwkIBwIGFQoJCAsCBBYCAwECHgEC +F4AWIQTRpm4aI7GCyZgPeIz7/MgqAV5zMAUCXaWe+gAKCRD7/MgqAV5zMG9sC/9U +2T3RrqEbw533FPNfEflhEVRIZ8gDXKM8hU6cqqEzCmzZT6xYTe6sv4y+PJBGXJFX +yhj0g6FDkSyboM5litOcTupURObVqMgA/Y4UKERznm4fzzH9qek85c4ljtLyNufe +doL2pp3vkGtn7eD0QFRaLLmnxPKQ/TlZKdLE1G3u8Uot8QHicaR6GnAdc5UXQJE3 +BiV7jZuDyWmZ1cUNwJkKL6oRtp+ZNDOQCrLNLecKHcgCqrpjSQG5oouba1I1Q6Vl +sP44dhA1nkmLHtxlTOzpeHj4jnk1FaXmyasurrrI5CgU/L2Oi39DGKTH/A/cywDN +4ZplIQ9zR8enkbXquUZvFDe+Xz+6xRXtb5MwQyWODB3nHw85HocLwRoIN9WdQEI+ +L8a/56AuOwhs8llkSuiITjR7r9SgKJC2WlAHl7E8lhJ3VDW3ELC56KH308d6mwOG +ZRAqIAKzM1T5FGjMBhq7ZV0eqdEntBh3EcOIfj2M8rg1MzJv+0mHZOIjByawikad +BVgEXaWc8gEMANYwv1xsYyunXYK0X1vY/rP1NNPvhLyLIE7NpK90YNBj+xS1ldGD +bUdZqZeef2xJe8gMQg05DoD1DF3GipZ0Ies65beh+d5hegb7N4pzh0LzrBrVNHar +29b5ExdI7i4iYD5TO6Vr/qTUOiAN/byqELEzAb+L+b2DVz/RoCm4PIp1DU9ewcc2 +WB38Ofqut3nLYA5tqJ9XvAiEQme+qAVcM3ZFcaMt4I4dXhDZZNg+D9LiTWcxdUPB +leu8iwDRjAgyAhPzpFp+nWoqWA81uIiULWD1Fj+IVoY3ZvgivoYOiEFBJ9lbb4te +g9m5UT/AaVDTWuHzbspVlbiVe+qyB77C2daWzNyx6UYBPLOo4r0t0c91kbNE5lgj +Z7xz6los0N1U8vq91EFSeQJoSQ62XWavYmlCLmdNT6BNfgh4icLsT7Vr1QMX9jzn +JtTPxdXytSdHvpSpULsqJ016l0dtmONcK3z9mj5N5z0k1tg1AH970TGYOe2aUcSx +IRDMXDOPyzEfjwARAQABAAv9F2CwsjS+Sjh1M1vegJbZjei4gF1HHpEM0K0PSXsp +SfVvpR4AoSJ4He6CXSMWg0ot8XKtDuZoV9jnJaES5UL9pMAD7JwIOqZm/DYVJM5h +OASCh1c356/wSbFbzRHPtUdZO9Q30WFNJM5pHbCJPjtNoRmRGkf71RxtvHBzy7np +Ga+W6U/NVKHw0i0CYwMI0YlKDakYW3Pm+QL+gHZFvngGweTod0f9l2VLLAmeQR/c ++EZs7lNumhuZ8mXcwhUc9JQIhOkpO+wreDysEFkAcsKbkQP3UDUsA1gFx9pbMzT0 +tr1oZq2a4QBtxShHzP/ph7KLpN+6qtjks3xB/yjTgaGmtrwM8tSe0wD1RwXS+/1o +BHpXTnQ7TfeOGUAu4KCoOQLv6ELpKWbRBLWuiPwMdbGpvVFALO8+kvKAg9/r+/ny +zM2GQHY+J3Jh5JxPiJnHfXNZjIKLbFbIPdSKNyJBuazXW8xIa//mEHMI5OcvsZBK +clAIp7LXzjEjKXIwHwDcTn9pBgDpdOKTHOtJ3JUKx0rWVsDH6wq6iKV/FTVSY5jl +zN+puOEsskF1Lfxn9JsJihAVO3yNsp6RvkKtyNlFazaCVKtDAmkjoh60XNxcNRqr +gCnwdpbgdHP6v/hvZY54ZaJjz6L2e8unNEkYLxDt8cmAyGPgH2XgL7giHIp9jrsQ +aS381gnYwNX6wE1aEikgtY91nqJjwPlibF9avSyYQoMtEqM/1UjTjB2KdD/MitK5 +fP0VpvuXpNYZedmyq4UOMwdkiNMGAOrfmOeT0olgLrTMT5H97Cn3Yxbk13uXHNu/ +ZUZZNe8s+QtuLfUlKAJtLEUutN33TlWQY522FV0m17S+b80xJib3yZVJteVurrh5 +HSWHAM+zghQAvCesg5CLXa2dNMkTCmZKgCBvfDLZuZbjFwnwCI6u/NhOY9egKuUf +SA/je/RXaT8m5VxLYMxwqQXKApzD87fv0tLPlVIEvjEsaf992tFEFSNPcG1l/jpd +5AVXw6kKuf85UkJtYR1x2MkQDrqY1QX/XMw00kt8y9kMZUre19aCArcmor+hDhRJ +E3Gt4QJrD9z/bICESw4b4z2DbgD/Xz9IXsA/r9cKiM1h5QMtXvuhyfVeM01enhxM +GbOH3gjqqGNKysx0UODGEwr6AV9hAd8RWXMchJLaExK9J5SRawSg671ObAU24SdY +vMQ9Z4kAQ2+1ReUZzf3ogSMRZtMT+d18gT6L90/y+APZIaoArLPhebIAGq39HLmJ +26x3z0WAgrpA1kNsjXEXkoiZGPLKIGoe3hqJAbYEGAEKACAWIQTRpm4aI7GCyZgP +eIz7/MgqAV5zMAUCXaWc8gIbDAAKCRD7/MgqAV5zMOn/C/9ugt+HZIwX308zI+QX +c5vDLReuzmJ3ieE0DMO/uNSC+K1XEioSIZP91HeZJ2kbT9nn9fuReuoff0T0Dief +rbwcIQQHFFkrqSp1K3VWmUGp2JrUsXFVdjy/fkBIjTd7c5boWljv/6wAsSfiv2V0 +JSM8EFU6TYXxswGjFVfc6X97tJNeIrXL+mpSmPPqy2bztcCCHkWS5lNLWQw+R7Vg +71Fe6yBSNVrqC2/imYG2J9zlowjx1XU63Wdgqp2Wxt0l8OmsB/W80S1fRF5G4SDH +s9HXglXXqPsBRZJYfP+VStm9L5P/sKjCcX6WtZR7yS6G8zj/X767MLK/djANvpPd +NVniEke6hM3CNBXYPAMhQBMWhCulcoz+0lxi8L34rMN+Dsbma96psdUrn7uLaB91 +6we0CTfF8qqm7BsVAgalon/UUiuMY80U3ueoj3okiSTiHIjD/YtpXSPioC8nMng7 +xqAY9Bwizt4FWgXuLm1a4+So4V9j1TRCXd12Uc2l2RNmgDE= +=miES +-----END PGP PRIVATE KEY BLOCK----- +` + const message = `From the grocery store we need: + +- tofu +- vegetables +- noodles + +` + key, err := NewKeyFromArmored(bob_key_armor) + if err != nil { + t.Error("error: ", err) + } + pgp := PGP() + signer, err := pgp.Sign().SigningKey(key).New() + if err != nil { + t.Error("error: ", err) + } + msg, err := signer.SignCleartext([]byte(message)) + if err != nil { + t.Error("error: ", err) + } + verifier, err := pgp.Verify().VerificationKey(key).New() + if err != nil { + t.Error("error: ", err) + } + res, err := verifier.VerifyCleartext(msg) + if err != nil { + t.Error("error: ", err) + } + if err = res.SignatureError(); err != nil { + t.Error("Should have no signature error, got:", err) + } + if !bytes.Equal(res.Cleartext(), []byte(message)) { + t.Errorf("Should be equal, got: %s", res.Cleartext()) + } +} + +func TestDetachedSignaturesLowRSABits(t *testing.T) { + const ricarda_key_armor = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xcA4BF2lnPIBAgDJNC6ahwp/G+kb5lKvpbR4MdEs5GTSCgY8aU7m/dIcANhtZds8 +VRAURBHWhDaG09gQGALulT/nZ0wDzGaBhCTjABEBAAEAAfoCqvt3NxUvjEoyAYLV +K2hSM67nXnvrwRBGGsteCr2Pe3ldAHgeqAcUk2pliX9HoAofuF8YQxZGWFBoqjay +XakRAQDZhkRQdhJkAIKJIgrZPVsVT9WLIqhypYiyTvGtzaAxyQEA7MrmSxsPMjus +oElGKNBM8McvFFUmrZvXTuLr/vB4l0sA/Ro5IitkUUWzHkp2+qvaA3DyvoEMNUX6 +mH5gar7YlEpJSHfNIUJvYiBCYWJiYWdlIDxib2JAb3BlbnBncC5leGFtcGxlPsLA +BwQTAQgAewWCXaWc8gILCQkQcIR/3xOecFpHFAAAAAAAHgAgc2FsdEBub3RhdGlv +bnMuc2VxdW9pYS1wZ3Aub3JnzYZEjm7s2Jd5hS2EXzHIRv+fbeHnlPuqR00RjkPr +K/cCFQgCmwECHgEWIQRmgRgCRqWoYJRHisxwhH/fE55wWgAAI/cB/RNhn9tIg8le +Jy60I8QvzdLtFLMGanxqZYhKxZQnnei1bCuUQoMxOsVX7KAIt1AFPQ+1xbhz43t1 +oaBoyxuRYkDHxJgEXaWc8gEMANYwv1xsYyunXYK0X1vY/rP1NNPvhLyLIE7NpK90 +YNBj+xS1ldGDbUdZqZeef2xJe8gMQg05DoD1DF3GipZ0Ies65beh+d5hegb7N4pz +h0LzrBrVNHar29b5ExdI7i4iYD5TO6Vr/qTUOiAN/byqELEzAb+L+b2DVz/RoCm4 +PIp1DU9ewcc2WB38Ofqut3nLYA5tqJ9XvAiEQme+qAVcM3ZFcaMt4I4dXhDZZNg+ +D9LiTWcxdUPBleu8iwDRjAgyAhPzpFp+nWoqWA81uIiULWD1Fj+IVoY3ZvgivoYO +iEFBJ9lbb4teg9m5UT/AaVDTWuHzbspVlbiVe+qyB77C2daWzNyx6UYBPLOo4r0t +0c91kbNE5lgjZ7xz6los0N1U8vq91EFSeQJoSQ62XWavYmlCLmdNT6BNfgh4icLs +T7Vr1QMX9jznJtTPxdXytSdHvpSpULsqJ016l0dtmONcK3z9mj5N5z0k1tg1AH97 +0TGYOe2aUcSxIRDMXDOPyzEfjwARAQABAAv9F2CwsjS+Sjh1M1vegJbZjei4gF1H +HpEM0K0PSXspSfVvpR4AoSJ4He6CXSMWg0ot8XKtDuZoV9jnJaES5UL9pMAD7JwI +OqZm/DYVJM5hOASCh1c356/wSbFbzRHPtUdZO9Q30WFNJM5pHbCJPjtNoRmRGkf7 +1RxtvHBzy7npGa+W6U/NVKHw0i0CYwMI0YlKDakYW3Pm+QL+gHZFvngGweTod0f9 +l2VLLAmeQR/c+EZs7lNumhuZ8mXcwhUc9JQIhOkpO+wreDysEFkAcsKbkQP3UDUs +A1gFx9pbMzT0tr1oZq2a4QBtxShHzP/ph7KLpN+6qtjks3xB/yjTgaGmtrwM8tSe +0wD1RwXS+/1oBHpXTnQ7TfeOGUAu4KCoOQLv6ELpKWbRBLWuiPwMdbGpvVFALO8+ +kvKAg9/r+/nyzM2GQHY+J3Jh5JxPiJnHfXNZjIKLbFbIPdSKNyJBuazXW8xIa//m +EHMI5OcvsZBKclAIp7LXzjEjKXIwHwDcTn9pBgDpdOKTHOtJ3JUKx0rWVsDH6wq6 +iKV/FTVSY5jlzN+puOEsskF1Lfxn9JsJihAVO3yNsp6RvkKtyNlFazaCVKtDAmkj +oh60XNxcNRqrgCnwdpbgdHP6v/hvZY54ZaJjz6L2e8unNEkYLxDt8cmAyGPgH2Xg +L7giHIp9jrsQaS381gnYwNX6wE1aEikgtY91nqJjwPlibF9avSyYQoMtEqM/1UjT +jB2KdD/MitK5fP0VpvuXpNYZedmyq4UOMwdkiNMGAOrfmOeT0olgLrTMT5H97Cn3 +Yxbk13uXHNu/ZUZZNe8s+QtuLfUlKAJtLEUutN33TlWQY522FV0m17S+b80xJib3 +yZVJteVurrh5HSWHAM+zghQAvCesg5CLXa2dNMkTCmZKgCBvfDLZuZbjFwnwCI6u +/NhOY9egKuUfSA/je/RXaT8m5VxLYMxwqQXKApzD87fv0tLPlVIEvjEsaf992tFE +FSNPcG1l/jpd5AVXw6kKuf85UkJtYR1x2MkQDrqY1QX/XMw00kt8y9kMZUre19aC +Arcmor+hDhRJE3Gt4QJrD9z/bICESw4b4z2DbgD/Xz9IXsA/r9cKiM1h5QMtXvuh +yfVeM01enhxMGbOH3gjqqGNKysx0UODGEwr6AV9hAd8RWXMchJLaExK9J5SRawSg +671ObAU24SdYvMQ9Z4kAQ2+1ReUZzf3ogSMRZtMT+d18gT6L90/y+APZIaoArLPh +ebIAGq39HLmJ26x3z0WAgrpA1kNsjXEXkoiZGPLKIGoe3hrCwfwEGAEIAnAFgl2l +nPIJEHCEf98TnnBaRxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdw +Lm9yZ6G8AriWc+q5U1C7JIGm8ekmYUh92vSQDsKhopZ3CS74ApsCwTygBBkBCABv +BYJdpZzyCRB8L6pN+Tw3skcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lh +LXBncC5vcmfhMitLHGdfOGNMsEzKEVLG2R/x0/fCsRh06PuEjAk1ihYhBB3c4V8J +IXzuLzs3YHwvqk35PDeyAAAhVgv9F8w8P5la8RB86QNSLMNScGsFVbtmOY5grBwd +wH94u9fhe4ZvxFX9PCx+dwIEKm1V0/1/auwa7dtKidtBoT1PgYE6liJc7K8p9/MU +k0h5ZwAesHY27rFDlFduxa1g4Mn5mfk/lhtnzsYCzCzpGt84T4xTeH6QMWwIPDQk +BlBIsdZ1idsTCnX08rSKsGi9Sz4hhytLxil9Yx8fcRA55OgPhWLILRNTKTJaY+8a +4Fp/6r+GD8HJEOJdNyNWjjGMmcrm+BCzz0q3duI8lFMqSZgJFltz7Hme0rLcw4dc +bBdQcW30Smb5I8FE2473w7PbOC9I/ifp/CuLI6OEUt40ZSGfIE4mhswcR4CIUPv6 +aPCITUCPHktQMezzBDkWllZG82GpDIwyW3l/K9Gy+zNKsZXa6Au1j7Dtly7f67Tw +iBp2E/2qMMoaqtADDzMfJuNTV1c7ApjMVzZ/+iFHcZeA3CNThNob0/7A8i/L7bYP +woAbDzVSzqvu/BLhpYifrL5KwEJtFiEEZoEYAkalqGCUR4rMcIR/3xOecFoAAESu +Af0YTasmaSgpJa+4BXZiinTBD3DhCr2aX69At9WQ5ULOsH+gn1CvADRHLMW2wLgC +KNs/Xw4PLUnp6wQO2d/6oZay +=ffeX +-----END PGP PRIVATE KEY BLOCK----- +` + const sig = `-----BEGIN PGP SIGNATURE----- + +wsE7BAABCgBvBYJkpnJFCRB8L6pN+Tw3skcUAAAAAAAeACBzYWx0QG5vdGF0aW9u +cy5zZXF1b2lhLXBncC5vcme7t8QmfP04d0zI8u8HUPmZcNbExpgA23j+/ICq8B3N +EhYhBB3c4V8JIXzuLzs3YHwvqk35PDeyAAA9fwwAtaSuGkMxdU3CyO1n8x7mBaZS +F2QNzvUKlhei1vPf5RaXTlNmlDWl/wF7aqRlrbdSHnqvi5lbhVVNP5LU62tS1J1N +WudlfoVhIexYOtoYOvOnsR3nBT95Mevk64UTVPs2rAfNDSD/0TKtK/gL+B9UBI2K +8UJwxcjtO116qwsO6gJq5XJjn0y34nNdIJH8Rr5I1jtl6h3Xlh2et58Yp9LmubNw +mrqvl+ES8bh8Q++KpesnswQnSZR9e4lp6FY6w7X6UDyxtJF9S0SBZnsduJtsoxUl +U8J1SUKwl0ATk9a+4+bwkwK9UJXFzwqEJvLoMe4U5QJKYazosr7Mxw197/iYOLXD +LlSSmcS1v5Oo37CVG+cZZnyiB1vra7x1fnpR+EA/bbsKkXXQjAepMhb61IFq44dL +j/6t75dByS9g29r2Fen+KFHh4QZrai9jEabepP9nKgNza7hIEeZtVdNyJfENUfQd +l6qOAQGHaxD0whADX2J53HqPUpcbidzw3jEKwWgI +=+Dhj +-----END PGP SIGNATURE----- +` + message := "Hello World :)" + + key, err := NewKeyFromArmored(ricarda_key_armor) + if err != nil { + t.Error("error: ", err) + } + pgp := PGP() + verifier, err := pgp.Verify().VerificationKey(key).New() + if err != nil { + t.Error("error: ", err) + } + res, err := verifier.VerifyDetached([]byte(message), []byte(sig), Armor) + if err != nil { + t.Error("error: ", err) + } + if err = res.SignatureError(); err == nil { + t.Error("Should have signature error", err) + } +} + +func TestDetachedSignaturesAndroid(t *testing.T) { + const ricarda_key_armor = `-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GopenPGP 2.1.1 +Comment: https://gopenpgp.org + +xsBNBF1BfxUBCADUpiiG3AhQK08E2nBmQ50XeztOWArmknINQV41pqGFW5VQkfbQ +3FYsANhLGqbDBQ0XxmocjKL7W7W8Y4xmHCGgkCUy6gAqGbi+sXY9Sl8xqQNHuZDh +WVdqT8+Rtv+DRxp/XrGkzC1U8CBYUmmKS92ldy0/zZIvgQXT6t5Q+v+BeUSv4jCs +nY3BE0UBOljtrTXlOcXRZHQxORWG+kon0qgcJERdwwzhxY6eT8jEfAfJY0hzQaYg ++6bj6ZR0zkMtY2Psq2M05kzEw4On/dezZETAu1e9fSqfk1mp+H6BeLJ9RUyrFK/P +qIO48+pU8CmAvTdx5eIihyOM16CFg/3GgV85ABEBAAHNMWFkYW10c3RAcHJvdG9u +bWFpbC5ibHVlIDxhZGFtdHN0QHByb3Rvbm1haWwuYmx1ZT7CwGgEEwEIABwFAl1B +fxUJEBHDHo5eB/TQAhsDAhkBAgsJAhUIAADHoggACDYDZkyTMZX69k9uoAygAQ75 +2kb52r0L3dSLge+hUelxJOiVUznbavzVhzjzF2FucXP0csOSJygHNejjS62BDtsX +iIoPiVDO1+Hr72X4gE277VeZ1b6VozJvKC0+H4fhg/EtkD07oVhHJRxOOVlgUXGK +nP2lz2ojny0McggvN+Nk8ILqt6ImlvEk6CnTs9XdmcmosMiQU+U+THKrKZ+5Yec8 +4kzlHG8ee7Tim2yn9n/FuBStrYkTJUsDuAL/LOfF9DnzTzukK6kqpDB6kDfMeYQJ +Lq+Tu642n74P0lqOO0Wy7imI/hxM1W8yqcNdafS7PCuGHD99mecdKWVeYHCCY87A +TQRdQX8VAQgAyAIj6ytLB1CUinP6f6aVKlLSM9e9Figv1CAw53NHeQBbmwzU5bZn +tE6UERnvJy6ul7hJr9Jop+O1/jA6zaGanF5wv0nEvTHcoYRpJ4QiJgiQxvhOdItH +29+jBV1F44xOzlGnEzFAv7GbPecKHAsQgX9qYCj+5ydcttQ29gWQ6nN23G03R3Lb +KRS9H2uw1SIYGgif8FgKpJemwJjuSibyViXTf3JC8ZUtYbq+vIXqATFFtbrUHfKM +AKlHo0uLYGq1rRINGR6Dmhu6bGhZonuW0na4+5Wh86kg9c/YI7jSIIspRRkH+v7+ +RXH51h8Rbc2Tiv64qy7cIJIH0Bk0lFAaIQARAQABwsBfBBgBCAATBQJdQX8VCRAR +wx6OXgf00AIbDAAAgvAIAGyLaHYTjiXG/ORIIAgdQhKBYOozrOS4EcOPNdMSPBwT +6P+BpNS/GD33pzANVKM9Mft8+NnePUFOR2f4QJrQ1VvSj9ko8P2sX7IAe7+rG614 +LQfzjG5R16KlSVOMFW0K2L8ZxumDdYl/N0BhgtZmB1lg1xY2TPHoDetznMnHG8sL +6u6vyhGl5a6qcW2g1urlF0VF/CEqg1lwAKhFHIFiNR+X6jCjg0KJa9MjAW6oICOx +oX0jp195mWix6suRJSWVK14uieT6uL5yYC5tZMz+t9rs7YxCkHxFRT1H5ZLHUD/r +93liqW+pzUx+bVdz5qNMb0ZonHZRLe3/Fzb19x8UMPc= +=6gp8 +-----END PGP PUBLIC KEY BLOCK----- +` + const sig = `-----BEGIN PGP SIGNATURE----- +Version: GopenPGP 2.4.10 +Comment: https://gopenpgp.org + +wsBzBAABCgAnBQJjocOZCZARwx6OXgf00BYhBCDPNjtY7JnnIuU+xBHDHo5eB/TQ +AACGgwf7Bx6J7JLZ2G6RFvr/wtl0DENZxUVS4H3wZPEIuVTh3/Lzd5BHfWN/mD+q +Sz0BcjRNxAI+nDY2/J8HPIibNg1NDlUgrgxK0NPLS1DMWmtoW3JTF5sfFMyiVGxo +RH4oluOe/UQcfxYTbMr8/EX8Gc9kdx4U7MqQNEc9CM5VIuxrfMpSZ2hvn5zlwexQ +WdnWjVWePpbwpltX98wTlAtU93XARUgeIMrzkhEBc1sNSg6/ynECLENm8EMxWQmj +9lpaROb2Fw50G7S1YjSUlc7WK+e4+IIP3Fqw/b21Kd1BasHS92OuHZNalbxyJA0F +V6Zkmvzj3h9CucLSJw1Bo6ZJTDbkBQ== +=fVs7 +-----END PGP SIGNATURE----- +` + message := "This is a test\nWith trailing spaces: \n With leading spaces\nWith trailing tabs:\t\t\n\tWith leading tabs\nWith trailing carriage returns:\r\n\rWith leading carriage returns\n\t \r With a mix \t\r\n" + message = internal.TrimEachLine(message) + key, err := NewKeyFromArmored(ricarda_key_armor) + if err != nil { + t.Error("error: ", err) + } + pgp := PGP() + verifier, err := pgp.Verify().VerificationKey(key).Utf8().New() + if err != nil { + t.Error("error: ", err) + } + res, err := verifier.VerifyDetached([]byte(message), []byte(sig), Armor) + if err != nil { + t.Error("error: ", err) + } + if err = res.SignatureError(); err != nil { + t.Error("Should have signature error", err) + } +} diff --git a/crypto/key.go b/crypto/key.go index 16c50d98..b9fdcd17 100644 --- a/crypto/key.go +++ b/crypto/key.go @@ -2,21 +2,20 @@ package crypto import ( "bytes" - "crypto" "crypto/sha256" "encoding/hex" + "encoding/json" "fmt" "io" - "math/big" "strconv" "strings" + "time" - "github.com/ProtonMail/gopenpgp/v2/armor" - "github.com/ProtonMail/gopenpgp/v2/constants" - "github.com/pkg/errors" - - openpgp "github.com/ProtonMail/go-crypto/openpgp" packet "github.com/ProtonMail/go-crypto/openpgp/packet" + openpgp "github.com/ProtonMail/go-crypto/openpgp/v2" + "github.com/ProtonMail/gopenpgp/v3/armor" + "github.com/ProtonMail/gopenpgp/v3/constants" + "github.com/pkg/errors" ) // Key contains a single private or public key. @@ -25,40 +24,81 @@ type Key struct { entity *openpgp.Entity } -// --- Create Key object +type KeyEncryptionProfile interface { + KeyEncryptionConfig() *packet.Config +} -// NewKeyFromArmoredReader reads an armored data into a key. -func NewKeyFromArmoredReader(r io.Reader) (key *Key, err error) { - key = &Key{} - err = key.readFrom(r, true) - if err != nil { - return nil, err - } +// --- Create Key object - return key, nil +// NewKeyFromReader reads binary or armored data into a Key object. +func NewKeyFromReader(r io.Reader) (key *Key, err error) { + return NewKeyFromReaderExplicit(r, Auto) } -// NewKeyFromReader reads binary data into a Key object. -func NewKeyFromReader(r io.Reader) (key *Key, err error) { +// NewKeyFromReaderExplicit reads binary or armored data into a Key object. +// Allows to set the encoding explicitly to avoid the armor check. +func NewKeyFromReaderExplicit(r io.Reader, encoding int8) (key *Key, err error) { + var armored bool key = &Key{} - err = key.readFrom(r, false) + switch encoding { + case Auto: + r, armored = armor.IsPGPArmored(r) + case Armor: + armored = true + case Bytes: + armored = false + default: + return nil, errors.New("gopenpgp: encoding is not supported") + } + err = key.readFrom(r, armored) if err != nil { return nil, err } - return key, nil } -// NewKey creates a new key from the first key in the unarmored binary data. +// NewKey creates a new key from the first key in the unarmored or armored binary data. +// Clones the binKeys data for go-mobile compatibility. func NewKey(binKeys []byte) (key *Key, err error) { return NewKeyFromReader(bytes.NewReader(clone(binKeys))) } +// NewKeyWithCloneFlag creates a new key from the first key in the unarmored or armored binary data. +func NewKeyWithCloneFlag(binKeys []byte, clone bool) (key *Key, err error) { + if clone { + return NewKey(binKeys) + } + return NewKeyFromReader(bytes.NewReader(binKeys)) +} + // NewKeyFromArmored creates a new key from the first key in an armored string. func NewKeyFromArmored(armored string) (key *Key, err error) { - return NewKeyFromArmoredReader(strings.NewReader(armored)) + return NewKeyFromReader(strings.NewReader(armored)) } +// NewPrivateKeyFromArmored creates a new secret key from the first key in an armored string +// and unlocks it with the password. +func NewPrivateKeyFromArmored(armored string, password []byte) (key *Key, err error) { + lockedKey, err := NewKeyFromArmored(armored) + if err != nil { + return + } + isLocked, err := lockedKey.IsLocked() + if err != nil { + return + } + if isLocked { + key, err = lockedKey.Unlock(password) + if err != nil { + return nil, err + } + } else { + key = lockedKey + } + return +} + +// NewKeyFromEntity creates a key from the provided go-crypto/openpgp entity. func NewKeyFromEntity(entity *openpgp.Entity) (*Key, error) { if entity == nil { return nil, errors.New("gopenpgp: nil entity provided") @@ -66,20 +106,12 @@ func NewKeyFromEntity(entity *openpgp.Entity) (*Key, error) { return &Key{entity: entity}, nil } -// GenerateRSAKeyWithPrimes generates a RSA key using the given primes. -func GenerateRSAKeyWithPrimes( - name, email string, - bits int, - primeone, primetwo, primethree, primefour []byte, -) (*Key, error) { - return generateKey(name, email, "rsa", bits, primeone, primetwo, primethree, primefour) -} - -// GenerateKey generates a key of the given keyType ("rsa" or "x25519"). -// If keyType is "rsa", bits is the RSA bitsize of the key. -// If keyType is "x25519" bits is unused. -func GenerateKey(name, email string, keyType string, bits int) (*Key, error) { - return generateKey(name, email, keyType, bits, nil, nil, nil, nil) +// generateKey generates a key with the given key-generation profile and security-level. +func generateKey(name, email string, clock Clock, profile KeyGenerationProfile, securityLevel int8, lifeTimeSec uint32) (*Key, error) { + config := profile.KeyGenerationConfig(securityLevel) + config.Time = NewConstantClock(clock().Unix()) + config.KeyLifetimeSecs = lifeTimeSec + return generateKeyWithConfig(name, email, "", config) } // --- Operate on key @@ -94,8 +126,8 @@ func (key *Key) Copy() (*Key, error) { return NewKey(serialized) } -// Lock locks a copy of the key. -func (key *Key) Lock(passphrase []byte) (*Key, error) { +// lock locks a copy of the key. +func (key *Key) lock(passphrase []byte, profile KeyEncryptionProfile) (*Key, error) { unlocked, err := key.IsUnlocked() if err != nil { return nil, err @@ -114,19 +146,9 @@ func (key *Key) Lock(passphrase []byte) (*Key, error) { return lockedKey, nil } - if lockedKey.entity.PrivateKey != nil && !lockedKey.entity.PrivateKey.Dummy() { - err = lockedKey.entity.PrivateKey.Encrypt(passphrase) - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: error in locking key") - } - } - - for _, sub := range lockedKey.entity.Subkeys { - if sub.PrivateKey != nil && !sub.PrivateKey.Dummy() { - if err := sub.PrivateKey.Encrypt(passphrase); err != nil { - return nil, errors.Wrap(err, "gopenpgp: error in locking sub key") - } - } + err = lockedKey.entity.EncryptPrivateKeys(passphrase, profile.KeyEncryptionConfig()) + if err != nil { + return nil, errors.Wrap(err, "gopenpgp: error in locking key") } locked, err := lockedKey.IsLocked() @@ -159,19 +181,9 @@ func (key *Key) Unlock(passphrase []byte) (*Key, error) { return nil, err } - if unlockedKey.entity.PrivateKey != nil && !unlockedKey.entity.PrivateKey.Dummy() { - err = unlockedKey.entity.PrivateKey.Decrypt(passphrase) - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: error in unlocking key") - } - } - - for _, sub := range unlockedKey.entity.Subkeys { - if sub.PrivateKey != nil && !sub.PrivateKey.Dummy() { - if err := sub.PrivateKey.Decrypt(passphrase); err != nil { - return nil, errors.Wrap(err, "gopenpgp: error in unlocking sub key") - } - } + err = unlockedKey.entity.DecryptPrivateKeys(passphrase) + if err != nil { + return nil, errors.New("gopenpgp: error in unlocking key") } isUnlocked, err := unlockedKey.IsUnlocked() @@ -212,10 +224,10 @@ func (key *Key) Armor() (string, error) { } if key.IsPrivate() { - return armor.ArmorWithType(serialized, constants.PrivateKeyHeader) + return armor.ArmorWithTypeChecksum(serialized, constants.PrivateKeyHeader, !key.isV6()) } - return armor.ArmorWithType(serialized, constants.PublicKeyHeader) + return armor.ArmorWithTypeChecksum(serialized, constants.PublicKeyHeader, !key.isV6()) } // ArmorWithCustomHeaders returns the armored key as a string, with @@ -226,7 +238,7 @@ func (key *Key) ArmorWithCustomHeaders(comment, version string) (string, error) return "", err } - return armor.ArmorWithTypeAndCustomHeaders(serialized, constants.PrivateKeyHeader, version, comment) + return armor.ArmorWithTypeAndCustomHeadersChecksum(serialized, constants.PrivateKeyHeader, version, comment, !key.isV6()) } // GetArmoredPublicKey returns the armored public keys from this keyring. @@ -236,7 +248,7 @@ func (key *Key) GetArmoredPublicKey() (s string, err error) { return "", err } - return armor.ArmorWithType(serialized, constants.PublicKeyHeader) + return armor.ArmorWithTypeChecksum(serialized, constants.PublicKeyHeader, !key.isV6()) } // GetArmoredPublicKeyWithCustomHeaders returns the armored public key as a string, with @@ -247,7 +259,7 @@ func (key *Key) GetArmoredPublicKeyWithCustomHeaders(comment, version string) (s return "", err } - return armor.ArmorWithTypeAndCustomHeaders(serialized, constants.PublicKeyHeader, version, comment) + return armor.ArmorWithTypeAndCustomHeadersChecksum(serialized, constants.PublicKeyHeader, version, comment, !key.isV6()) } // GetPublicKey returns the unarmored public keys from this keyring. @@ -263,27 +275,32 @@ func (key *Key) GetPublicKey() (b []byte, err error) { // --- Key object properties // CanVerify returns true if any of the subkeys can be used for verification. -func (key *Key) CanVerify() bool { - _, canVerify := key.entity.SigningKey(getNow()) +func (key *Key) CanVerify(unixTime int64) bool { + _, canVerify := key.entity.SigningKey(time.Unix(unixTime, 0), nil) return canVerify } // CanEncrypt returns true if any of the subkeys can be used for encryption. -func (key *Key) CanEncrypt() bool { - _, canEncrypt := key.entity.EncryptionKey(getNow()) +func (key *Key) CanEncrypt(unixTime int64) bool { + _, canEncrypt := key.entity.EncryptionKey(time.Unix(unixTime, 0), nil) return canEncrypt } // IsExpired checks whether the key is expired. -func (key *Key) IsExpired() bool { - i := key.entity.PrimaryIdentity() - return key.entity.PrimaryKey.KeyExpired(i.SelfSignature, getNow()) || // primary key has expired - i.SelfSignature.SigExpired(getNow()) // user ID self-signature has expired +func (key *Key) IsExpired(unixTime int64) bool { + current := time.Unix(unixTime, 0) + sig, err := key.entity.PrimarySelfSignature(time.Time{}, &packet.Config{}) + if err != nil { + return true + } + return key.entity.PrimaryKey.KeyExpired(sig, current) || // primary key has expired + sig.SigExpired(current) // user ID self-signature has expired } // IsRevoked checks whether the key or the primary identity has a valid revocation signature. -func (key *Key) IsRevoked() bool { - return key.entity.Revoked(getNow()) || key.entity.PrimaryIdentity().Revoked(getNow()) +func (key *Key) IsRevoked(unixTime int64) bool { + current := time.Unix(unixTime, 0) + return key.entity.Revoked(current) } // IsPrivate returns true if the key is private. @@ -294,7 +311,7 @@ func (key *Key) IsPrivate() bool { // IsLocked checks if a private key is locked. func (key *Key) IsLocked() (bool, error) { if key.entity.PrivateKey == nil { - return true, errors.New("gopenpgp: a public key cannot be locked") + return false, errors.New("gopenpgp: a public key cannot be locked") } encryptedKeys := 0 @@ -343,7 +360,11 @@ func (key *Key) Check() (bool, error) { // PrintFingerprints is a debug helper function that prints the key and subkey fingerprints. func (key *Key) PrintFingerprints() { for _, subKey := range key.entity.Subkeys { - if !subKey.Sig.FlagsValid || subKey.Sig.FlagEncryptStorage || subKey.Sig.FlagEncryptCommunications { + binding, err := subKey.LatestValidBindingSignature(time.Time{}, &packet.Config{}) + if err != nil { + continue + } + if !binding.FlagsValid || binding.FlagEncryptStorage || binding.FlagEncryptCommunications { fmt.Println("SubKey:" + hex.EncodeToString(subKey.PublicKey.Fingerprint)) } } @@ -356,6 +377,7 @@ func (key *Key) GetHexKeyID() string { } // GetKeyID returns the key ID, encoded as 8-byte int. +// Does not work for go-mobile clients, use GetHexKeyID instead. func (key *Key) GetKeyID() uint64 { return key.entity.PrimaryKey.KeyId } @@ -365,6 +387,11 @@ func (key *Key) GetFingerprint() string { return hex.EncodeToString(key.entity.PrimaryKey.Fingerprint) } +// GetFingerprintBytes gets the fingerprint from the key as a byte slice. +func (key *Key) GetFingerprintBytes() []byte { + return key.entity.PrimaryKey.Fingerprint +} + // GetSHA256Fingerprints computes the SHA256 fingerprints of the key and subkeys. func (key *Key) GetSHA256Fingerprints() (fingerprints []string) { fingerprints = append(fingerprints, hex.EncodeToString(getSHA256FingerprintBytes(key.entity.PrimaryKey))) @@ -374,11 +401,23 @@ func (key *Key) GetSHA256Fingerprints() (fingerprints []string) { return } +// GetJsonSHA256Fingerprints returns the SHA256 fingerprints of key and subkeys +// encoded in JSON, for gomobile clients that cannot handle arrays. +func (key *Key) GetJsonSHA256Fingerprints() ([]byte, error) { + return json.Marshal(key.GetSHA256Fingerprints()) +} + // GetEntity gets x/crypto Entity object. +// Not supported on go-mobile clients. func (key *Key) GetEntity() *openpgp.Entity { return key.entity } +// GetVersion returns the OpenPGP key packet version of this key. +func (key *Key) GetVersion() int { + return key.entity.PrimaryKey.Version +} + // ToPublic returns the corresponding public key of the given private key. func (key *Key) ToPublic() (publicKey *Key, err error) { if !key.IsPrivate() { @@ -394,6 +433,13 @@ func (key *Key) ToPublic() (publicKey *Key, err error) { return } +func (key *Key) isV6() bool { + if key == nil || key.entity == nil { + return false + } + return key.entity.PrimaryKey.Version == 6 +} + // --- Internal methods // getSHA256FingerprintBytes computes the SHA256 fingerprint of a public key @@ -411,6 +457,7 @@ func getSHA256FingerprintBytes(pk *packet.PublicKey) []byte { func (key *Key) readFrom(r io.Reader, armored bool) error { var err error var entities openpgp.EntityList + if armored { entities, err = openpgp.ReadArmoredKeyRing(r) } else { @@ -432,46 +479,14 @@ func (key *Key) readFrom(r io.Reader, armored bool) error { return nil } -func generateKey( - name, email string, - keyType string, - bits int, - prime1, prime2, prime3, prime4 []byte, +func generateKeyWithConfig( + name, email, comments string, + config *packet.Config, ) (*Key, error) { if len(email) == 0 && len(name) == 0 { return nil, errors.New("gopenpgp: neither name nor email set.") } - - comments := "" - - cfg := &packet.Config{ - Algorithm: packet.PubKeyAlgoRSA, - RSABits: bits, - Time: getKeyGenerationTimeGenerator(), - DefaultHash: crypto.SHA256, - DefaultCipher: packet.CipherAES256, - DefaultCompressionAlgo: packet.CompressionZLIB, - } - - if keyType == "x25519" { - cfg.Algorithm = packet.PubKeyAlgoEdDSA - } - - if prime1 != nil && prime2 != nil && prime3 != nil && prime4 != nil { - var bigPrimes [4]*big.Int - bigPrimes[0] = new(big.Int) - bigPrimes[0].SetBytes(prime1) - bigPrimes[1] = new(big.Int) - bigPrimes[1].SetBytes(prime2) - bigPrimes[2] = new(big.Int) - bigPrimes[2].SetBytes(prime3) - bigPrimes[3] = new(big.Int) - bigPrimes[3].SetBytes(prime4) - - cfg.RSAPrimes = bigPrimes[:] - } - - newEntity, err := openpgp.NewEntity(name, comments, email, cfg) + newEntity, err := openpgp.NewEntity(name, comments, email, config) if err != nil { return nil, errors.Wrap(err, "gopengpp: error in encoding new entity") } diff --git a/crypto/key_clear.go b/crypto/key_clear.go index e46eb89d..b19aae86 100644 --- a/crypto/key_clear.go +++ b/crypto/key_clear.go @@ -1,7 +1,7 @@ package crypto import ( - "crypto/dsa" //nolint:staticcheck + "crypto/dsa" "crypto/rsa" "errors" "math/big" @@ -16,11 +16,13 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/x448" ) +// Clear zeroes the sensitive data in the session key. func (sk *SessionKey) Clear() (ok bool) { clearMem(sk.Key) return true } +// ClearPrivateParams zeroes the sensitive data in the key. func (key *Key) ClearPrivateParams() (ok bool) { num := key.clearPrivateWithSubkeys() key.entity.PrivateKey = nil diff --git a/crypto/key_generation.go b/crypto/key_generation.go new file mode 100644 index 00000000..8eb9afea --- /dev/null +++ b/crypto/key_generation.go @@ -0,0 +1,33 @@ +package crypto + +import ( + "github.com/ProtonMail/go-crypto/openpgp/packet" +) + +// Integer enum for go-mobile compatibility. +const ( + // KeyGenerationRSA4096 allows to override the output key algorithm in key generation to rsa 4096. + KeyGenerationRSA4096 int = 1 + // KeyGenerationC25519 allows to override the output key algorithm in key generation to curve25519. + KeyGenerationC25519 int = 2 + // KeyGenerationC25519 allows to override the output key algorithm in key generation to curve25519 crypto refresh. + KeyGenerationC25519Refresh int = 3 + // KeyGenerationC448 allows to override the output key algorithm in key generation to curve448. + KeyGenerationC448 int = 4 + // KeyGenerationC448Refresh allows to override the output key algorithm in key generation to curve448 crypto refresh. + KeyGenerationC448Refresh int = 5 +) + +type KeyGenerationProfile interface { + KeyGenerationConfig(securityLevel int8) *packet.Config +} + +// PGPKeyGeneration is an interface for generating pgp keys with GopenPGP. +// Use the KeyGenerationBuilder to create a handle that implements PGPKeyGeneration. +type PGPKeyGeneration interface { + // GenerateKey generates a pgp key with the standard security level. + GenerateKey() (*Key, error) + // GenerateKeyWithSecurity generates a pgp key with the given security level. + // The argument security allows to set the security level, either standard or high. + GenerateKeyWithSecurity(securityLevel int8) (*Key, error) +} diff --git a/crypto/key_generation_handle.go b/crypto/key_generation_handle.go new file mode 100644 index 00000000..3e463a39 --- /dev/null +++ b/crypto/key_generation_handle.go @@ -0,0 +1,112 @@ +package crypto + +import ( + "github.com/ProtonMail/go-crypto/openpgp/packet" + openpgp "github.com/ProtonMail/go-crypto/openpgp/v2" + "github.com/ProtonMail/gopenpgp/v3/constants" + "github.com/pkg/errors" +) + +type identity struct { + name, comment, email string +} + +type keyGenerationHandle struct { + identities []identity + keyLifetimeSecs uint32 + overrideAlgorithm int + profile KeyGenerationProfile + clock Clock +} + +// --- Default key generation handle to build from + +func defaultKeyGenerationHandle(profile KeyGenerationProfile, clock Clock) *keyGenerationHandle { + return &keyGenerationHandle{ + clock: clock, + profile: profile, + } +} + +// --- Implements PGPKeyGeneration interface + +// GenerateKey generates a pgp key with the standard security level. +func (kgh *keyGenerationHandle) GenerateKey() (key *Key, err error) { + return kgh.GenerateKeyWithSecurity(constants.StandardSecurity) +} + +// GenerateKeyWithSecurity generates a pgp key with the given security level. +// The argument security allows to set the security level, either standard or high. +func (kgh *keyGenerationHandle) GenerateKeyWithSecurity(security int8) (key *Key, err error) { + config := kgh.profile.KeyGenerationConfig(security) + updateConfig(config, kgh.overrideAlgorithm) + config.Time = NewConstantClock(kgh.clock().Unix()) + config.KeyLifetimeSecs = kgh.keyLifetimeSecs + key = &Key{} + + if len(kgh.identities) == 0 { + if config.V6() { + key.entity, err = openpgp.NewEntityWithoutId(config) + } else { + return nil, errors.New("gopenpgp: non-v6 key requires a user id") + } + } else { + if err = kgh.identities[0].valid(); err != nil { + return nil, err + } + key.entity, err = openpgp.NewEntity( + kgh.identities[0].name, + kgh.identities[0].comment, + kgh.identities[0].email, + config, + ) + } + if err != nil { + return nil, errors.Wrap(err, "gopengpp: error in creating new entity") + } + + for id := 1; id < len(kgh.identities); id++ { + if err = kgh.identities[id].valid(); err != nil { + return nil, err + } + err = key.entity.AddUserId( + kgh.identities[id].name, + kgh.identities[id].comment, + kgh.identities[id].email, + config, + ) + if err != nil { + return nil, errors.Wrap(err, "gopengpp: error in adding user id") + } + } + + if key.entity.PrivateKey == nil { + return nil, errors.New("gopenpgp: error in generating private key") + } + return key, nil +} + +func (id identity) valid() error { + if len(id.email) == 0 && len(id.name) == 0 { + return errors.New("gopenpgp: neither name nor email set in user id") + } + return nil +} + +func updateConfig(config *packet.Config, algorithm int) { + switch algorithm { + case KeyGenerationRSA4096: + config.Algorithm = packet.PubKeyAlgoRSA + config.RSABits = 4096 + case KeyGenerationC25519: + config.Algorithm = packet.PubKeyAlgoEdDSA + config.Curve = packet.Curve25519 + case KeyGenerationC25519Refresh: + config.Algorithm = packet.PubKeyAlgoEd25519 + case KeyGenerationC448: + config.Algorithm = packet.PubKeyAlgoEdDSA + config.Curve = packet.Curve448 + case KeyGenerationC448Refresh: + config.Algorithm = packet.PubKeyAlgoEd448 + } +} diff --git a/crypto/key_generation_handle_builder.go b/crypto/key_generation_handle_builder.go new file mode 100644 index 00000000..51277b4d --- /dev/null +++ b/crypto/key_generation_handle_builder.go @@ -0,0 +1,52 @@ +package crypto + +// KeyGenerationBuilder allows to configure a key generation handle to generate OpenPGP keys. +type KeyGenerationBuilder struct { + handle *keyGenerationHandle + defaultClock Clock +} + +func newKeyGenerationBuilder(profile KeyGenerationProfile, clock Clock) *KeyGenerationBuilder { + return &KeyGenerationBuilder{ + handle: defaultKeyGenerationHandle(profile, clock), + defaultClock: clock, + } +} + +// GenerationTime sets the key generation time to the given unixTime. +func (kgb *KeyGenerationBuilder) GenerationTime(unixTime int64) *KeyGenerationBuilder { + kgb.handle.clock = NewConstantClock(unixTime) + return kgb +} + +// Lifetime sets the key lifetime to the given value in seconds. +// The lifetime defaults to zero i.e., infinite lifetime. +func (kgb *KeyGenerationBuilder) Lifetime(seconds int32) *KeyGenerationBuilder { + kgb.handle.keyLifetimeSecs = uint32(seconds) + return kgb +} + +// AddUserId adds the provided user identity to any generated key. +func (kgb *KeyGenerationBuilder) AddUserId(name, email string) *KeyGenerationBuilder { + kgb.handle.identities = append(kgb.handle.identities, identity{name, "", email}) + return kgb +} + +// OverrideProfileAlgorithm allows to override the algorithm of the output key instead of using the profile's +// algorithm with the respective security level. +// +// Allowed inputs (integer enum for go-mobile compatibility): +// crypto.KeyGenerationRSA4096, crypto.KeyGenerationC25519, crypto.KeyGenerationC25519Refresh +// crypto.KeyGenerationC448, crypto.KeyGenerationC448Refresh. +func (kgb *KeyGenerationBuilder) OverrideProfileAlgorithm(algorithm int) *KeyGenerationBuilder { + kgb.handle.overrideAlgorithm = algorithm + return kgb +} + +// New creates a new key generation handle from the internal configuration +// that allows to generate pgp keys. +func (kgb *KeyGenerationBuilder) New() PGPKeyGeneration { + handle := kgb.handle + kgb.handle = defaultKeyGenerationHandle(kgb.handle.profile, kgb.defaultClock) + return handle +} diff --git a/crypto/key_test.go b/crypto/key_test.go index e80c6c0b..c46ef6ea 100644 --- a/crypto/key_test.go +++ b/crypto/key_test.go @@ -1,17 +1,17 @@ package crypto import ( - "crypto/rsa" - "encoding/base64" - "io/ioutil" + "io" "regexp" "strings" "testing" + "time" - "github.com/ProtonMail/go-crypto/openpgp" "github.com/ProtonMail/go-crypto/openpgp/armor" "github.com/ProtonMail/go-crypto/openpgp/packet" - + openpgp "github.com/ProtonMail/go-crypto/openpgp/v2" + "github.com/ProtonMail/gopenpgp/v3/constants" + "github.com/ProtonMail/gopenpgp/v3/profile" "github.com/stretchr/testify/assert" ) @@ -29,12 +29,26 @@ var ( func initGenerateKeys() { var err error - keyTestRSA, err = GenerateKey(keyTestName, keyTestDomain, "rsa", 1024) + clock := NewConstantClock(testTime) + keyTestRSA, err = generateKey( + keyTestName, + keyTestDomain, + clock, + profile.RFC4880(), + constants.StandardSecurity, + 0, + ) if err != nil { panic("Cannot generate RSA key:" + err.Error()) } - - keyTestEC, err = GenerateKey(keyTestName, keyTestDomain, "x25519", 256) + keyTestEC, err = generateKey( + keyTestName, + keyTestDomain, + clock, + profile.Default(), + constants.StandardSecurity, + 0, + ) if err != nil { panic("Cannot generate EC key:" + err.Error()) } @@ -42,7 +56,7 @@ func initGenerateKeys() { func initArmoredKeys() { var err error - lockedRSA, err := keyTestRSA.Lock(keyTestPassphrase) + lockedRSA, err := testPGP.LockKey(keyTestRSA, keyTestPassphrase) if err != nil { panic("Cannot lock RSA key:" + err.Error()) } @@ -52,7 +66,7 @@ func initArmoredKeys() { panic("Cannot armor protected RSA key:" + err.Error()) } - lockedEC, err := keyTestEC.Lock(keyTestPassphrase) + lockedEC, err := testPGP.LockKey(keyTestEC, keyTestPassphrase) if err != nil { panic("Cannot lock EC key:" + err.Error()) } @@ -75,7 +89,7 @@ func TestArmorKeys(t *testing.T) { t.Fatal("Cannot armor unprotected EC key:" + err.Error()) } - rTest := regexp.MustCompile(`(?s)^-----BEGIN PGP PRIVATE KEY BLOCK-----.*Version: GopenPGP [0-9]+\.[0-9]+\.[0-9]+.*-----END PGP PRIVATE KEY BLOCK-----$`) + rTest := regexp.MustCompile(`(?s)^-----BEGIN PGP PRIVATE KEY BLOCK-----.*-----END PGP PRIVATE KEY BLOCK-----$`) assert.Regexp(t, rTest, noPasswordRSA) assert.Regexp(t, rTest, noPasswordEC) assert.Regexp(t, rTest, keyTestArmoredRSA) @@ -119,7 +133,7 @@ func TestLockUnlockKeys(t *testing.T) { t.Fatal("Should not be able to unlock public key:") } - _, err = publicKey.Lock(keyTestPassphrase) + _, err = testPGP.LockKey(publicKey, keyTestPassphrase) if err == nil { t.Fatal("Should not be able to lock public key:") } @@ -169,7 +183,7 @@ func testLockUnlockKey(t *testing.T, armoredKey string, pass []byte) { } // re-lock key - relockedKey, err := unlockedKey.Lock(keyTestPassphrase) + relockedKey, err := testPGP.LockKey(unlockedKey, keyTestPassphrase) if err != nil { t.Fatal("Cannot lock key:", err) } @@ -204,8 +218,8 @@ func ExampleKey_PrintFingerprints() { } func TestIsExpired(t *testing.T) { - assert.Exactly(t, false, keyTestRSA.IsExpired()) - assert.Exactly(t, false, keyTestEC.IsExpired()) + assert.False(t, keyTestRSA.IsExpired(testTime)) + assert.False(t, keyTestEC.IsExpired(testTime)) expiredKey, err := NewKeyFromArmored(readTestFile("key_expiredKey", false)) if err != nil { @@ -217,53 +231,8 @@ func TestIsExpired(t *testing.T) { t.Fatal("Cannot unarmor future key:", err) } - assert.Exactly(t, true, expiredKey.IsExpired()) - assert.Exactly(t, true, futureKey.IsExpired()) -} - -func TestGenerateKeyWithPrimes(t *testing.T) { - prime1, _ := base64.StdEncoding.DecodeString( - "/thF8zjjk6fFx/y9NId35NFx8JTA7jvHEl+gI0dp9dIl9trmeZb+ESZ8f7bNXUmTI8j271kyenlrVJiqwqk80Q==") - prime2, _ := base64.StdEncoding.DecodeString( - "0HyyG/TShsw7yObD+DDP9Ze39ye1Redljx+KOZ3iNDmuuwwI1/5y44rD/ezAsE7A188NsotMDTSy5xtfHmu0xQ==") - prime3, _ := base64.StdEncoding.DecodeString( - "3OyJpAdnQXNjPNzI1u3BWDmPrzWw099E0UfJj5oJJILSbsAg/DDrmrdrIZDt7f24d06HCnTErCNWjvFJ3Kdq4w==") - prime4, _ := base64.StdEncoding.DecodeString( - "58UEDXTX29Q9JqvuE3Tn+Qj275CXBnJbA8IVM4d05cPYAZ6H43bPN01pbJqJTJw/cuFxs+8C+HNw3/MGQOExqw==") - - staticRsaKey, err := GenerateRSAKeyWithPrimes(keyTestName, keyTestDomain, 1024, prime1, prime2, prime3, prime4) - if err != nil { - t.Fatal("Cannot generate RSA key with primes:", err) - } - - pk, ok := staticRsaKey.entity.PrivateKey.PrivateKey.(*rsa.PrivateKey) - assert.True(t, ok) - assert.Exactly(t, prime1, pk.Primes[0].Bytes()) - assert.Exactly(t, prime2, pk.Primes[1].Bytes()) -} - -func TestFailCheckIntegrity25519(t *testing.T) { - failCheckIntegrity(t, "x25519", 0) -} - -func TestFailCheckIntegrityRSA(t *testing.T) { - failCheckIntegrity(t, "rsa", 2048) -} - -func failCheckIntegrity(t *testing.T, keyType string, bits int) { - k1, _ := GenerateKey(keyTestName, keyTestDomain, keyType, bits) - k2, _ := GenerateKey(keyTestName, keyTestDomain, keyType, bits) - - k1.entity.PrivateKey.PrivateKey = k2.entity.PrivateKey.PrivateKey // Swap private keys - - serialized, err := k1.Serialize() - if err != nil { - t.Fatal("Expected no error while serializing keyring, got:", err) - } - - _, err = NewKey(serialized) - - assert.Error(t, err) + assert.True(t, expiredKey.IsExpired(testTime)) + assert.True(t, futureKey.IsExpired(testTime)) } func TestGetPublicKey(t *testing.T) { @@ -309,12 +278,12 @@ func TestGetArmoredPublicKey(t *testing.T) { assert.Exactly(t, expected.Type, block.Type) - b, err := ioutil.ReadAll(block.Body) + b, err := io.ReadAll(block.Body) if err != nil { t.Fatal("Expected no error while reading armored public key body, got:", err) } - eb, err := ioutil.ReadAll(expected.Body) + eb, err := io.ReadAll(expected.Body) if err != nil { t.Fatal("Expected no error while reading expected armored public key body, got:", err) } @@ -333,7 +302,6 @@ func TestGetArmoredPublicKey(t *testing.T) { assert.False(t, decodedKey.IsPrivate()) assert.True(t, keyTestRSA.IsPrivate()) - assert.Contains(t, publicKey, "Version: GopenPGP") privateFingerprint := keyTestRSA.GetFingerprint() publicFingerprint := decodedKey.GetFingerprint() @@ -382,7 +350,11 @@ func TestGetEntity(t *testing.T) { t.Fatal("Cannot unarmor key:", err) } entity := publicKey.GetEntity() - assert.True(t, entity.PrimaryIdentity().SelfSignature.FlagsValid) + selfSig, err := entity.PrimarySelfSignature(time.Unix(testTime, 0), &packet.Config{}) + if err != nil { + t.Fatal("Expected no error, got: ", err) + } + assert.True(t, selfSig.FlagsValid) assert.IsType(t, &openpgp.Entity{}, entity) } @@ -403,35 +375,32 @@ func TestToPublic(t *testing.T) { } func TestKeyCapabilities(t *testing.T) { - assert.True(t, keyTestEC.CanVerify()) - assert.True(t, keyTestEC.CanEncrypt()) - assert.True(t, keyTestRSA.CanVerify()) - assert.True(t, keyTestRSA.CanEncrypt()) + assert.True(t, keyTestEC.CanVerify(testTime)) + assert.True(t, keyTestEC.CanEncrypt(testTime)) + assert.True(t, keyTestRSA.CanVerify(testTime)) + assert.True(t, keyTestRSA.CanEncrypt(testTime)) publicKey, err := keyTestEC.ToPublic() if err != nil { t.Fatal("Cannot make key public:", err) } - assert.True(t, publicKey.CanVerify()) - assert.True(t, publicKey.CanEncrypt()) + assert.True(t, publicKey.CanVerify(testTime)) + assert.True(t, publicKey.CanEncrypt(testTime)) } -func TestRevokedKeyCapabilities(t *testing.T) { - pgp.latestServerTime = 1632219895 - defer func() { - pgp.latestServerTime = testTime - }() +const testRevokedKeyCapabilitiesTime = 1632219895 +func TestRevokedKeyCapabilities(t *testing.T) { revokedKey, err := NewKeyFromArmored(readTestFile("key_revoked", false)) if err != nil { t.Fatal("Cannot unarmor key:", err) } - assert.False(t, revokedKey.CanVerify()) - assert.False(t, revokedKey.CanEncrypt()) - assert.False(t, revokedKey.IsExpired()) - assert.True(t, revokedKey.IsRevoked()) + assert.False(t, revokedKey.CanVerify(testRevokedKeyCapabilitiesTime)) + assert.False(t, revokedKey.CanEncrypt(testRevokedKeyCapabilitiesTime)) + assert.False(t, revokedKey.IsExpired(testRevokedKeyCapabilitiesTime)) + assert.True(t, revokedKey.IsRevoked(testRevokedKeyCapabilitiesTime)) } func TestUnlockMismatchingKey(t *testing.T) { @@ -446,9 +415,13 @@ func TestUnlockMismatchingKey(t *testing.T) { } func TestKeyCompression(t *testing.T) { + selfSig, err := keyTestEC.entity.PrimarySelfSignature(time.Time{}, &packet.Config{}) + if err != nil { + t.Fatal("no error expected, got: ", err) + } assert.Equal( t, []uint8{uint8(packet.CompressionNone), uint8(packet.CompressionZLIB)}, - keyTestEC.entity.PrimaryIdentity().SelfSignature.PreferredCompression, + selfSig.PreferredCompression, ) } diff --git a/crypto/keyring.go b/crypto/keyring.go index e6b2f68a..35d01333 100644 --- a/crypto/keyring.go +++ b/crypto/keyring.go @@ -2,10 +2,11 @@ package crypto import ( "bytes" + "encoding/json" "time" - "github.com/ProtonMail/go-crypto/openpgp" "github.com/ProtonMail/go-crypto/openpgp/packet" + openpgp "github.com/ProtonMail/go-crypto/openpgp/v2" "github.com/pkg/errors" ) @@ -20,8 +21,8 @@ type KeyRing struct { // Identity contains the name and the email of a key holder. type Identity struct { - Name string - Email string + Name string `json:"name"` + Email string `json:"email"` } // --- New keyrings @@ -75,6 +76,7 @@ func NewKeyRingFromBinary(binKeys []byte) (*KeyRing, error) { // --- Extract keys from keyring // GetKeys returns openpgp keys contained in this KeyRing. +// Not supported on go mobile clients. func (keyRing *KeyRing) GetKeys() []*Key { keys := make([]*Key, keyRing.CountEntities()) for i, entity := range keyRing.entities { @@ -91,26 +93,27 @@ func (keyRing *KeyRing) GetKey(n int) (*Key, error) { return &Key{keyRing.entities[n]}, nil } -// getSigningEntity returns first private unlocked signing entity from keyring. -func (keyRing *KeyRing) getSigningEntity() (*openpgp.Entity, error) { - var signEntity *openpgp.Entity - +func (keyRing *KeyRing) signingEntities() ([]*openpgp.Entity, error) { + var signEntity []*openpgp.Entity for _, e := range keyRing.entities { // Entity.PrivateKey must be a signing key - if e.PrivateKey != nil { - if !e.PrivateKey.Encrypted { - signEntity = e - break - } + if e.PrivateKey != nil && !e.PrivateKey.Encrypted { + signEntity = append(signEntity, e) + } else { + return nil, errors.New("gopenpgp: signing entity does not contain unencrypted private key") } } - if signEntity == nil { - return nil, errors.New("gopenpgp: cannot sign message, unable to unlock signer key") - } - return signEntity, nil } +// getEntities returns the internal EntityList if the key ring is not nil. +func (keyRing *KeyRing) getEntities() openpgp.EntityList { + if keyRing == nil { + return nil + } + return keyRing.entities +} + // Serialize serializes a KeyRing to binary data. func (keyRing *KeyRing) Serialize() ([]byte, error) { var buffer bytes.Buffer @@ -134,15 +137,30 @@ func (keyRing *KeyRing) Serialize() ([]byte, error) { // CountEntities returns the number of entities in the keyring. func (keyRing *KeyRing) CountEntities() int { + if keyRing == nil { + return 0 + } return len(keyRing.entities) } // CountDecryptionEntities returns the number of entities in the keyring. -func (keyRing *KeyRing) CountDecryptionEntities() int { - return len(keyRing.entities.DecryptionKeys()) +// Takes the current time for checking the keys in unix time format. +// If the unix time is zero, time checks are ignored. +func (keyRing *KeyRing) CountDecryptionEntities(unixTime int64) int { + var count int + var checkTime time.Time + if unixTime != 0 { + checkTime = time.Unix(unixTime, 0) + } + for _, entity := range keyRing.entities { + decryptionKeys := entity.DecryptionKeys(0, checkTime, &packet.Config{}) + count += len(decryptionKeys) + } + return count } // GetIdentities returns the list of identities associated with this key ring. +// Not supported on go-mobile clients use keyRing.GetIdentitiesJson() instead. func (keyRing *KeyRing) GetIdentities() []*Identity { var identities []*Identity for _, e := range keyRing.entities { @@ -156,11 +174,22 @@ func (keyRing *KeyRing) GetIdentities() []*Identity { return identities } +// GetIdentitiesJson returns the list of identities associated with this key ring encoded as json. +// Returns nil if an encoding error occurs. +// Helper function for go-mobile clients. +func (keyRing *KeyRing) GetIdentitiesJson() []byte { + identitiesJson, err := json.Marshal(keyRing.GetIdentities()) + if err != nil { + return nil + } + return identitiesJson +} + // CanVerify returns true if any of the keys in the keyring can be used for verification. -func (keyRing *KeyRing) CanVerify() bool { +func (keyRing *KeyRing) CanVerify(unixTime int64) bool { keys := keyRing.GetKeys() for _, key := range keys { - if key.CanVerify() { + if key.CanVerify(unixTime) { return true } } @@ -168,10 +197,10 @@ func (keyRing *KeyRing) CanVerify() bool { } // CanEncrypt returns true if any of the keys in the keyring can be used for encryption. -func (keyRing *KeyRing) CanEncrypt() bool { +func (keyRing *KeyRing) CanEncrypt(unixTime int64) bool { keys := keyRing.GetKeys() for _, key := range keys { - if key.CanEncrypt() { + if key.CanEncrypt(unixTime) { return true } } @@ -179,6 +208,7 @@ func (keyRing *KeyRing) CanEncrypt() bool { } // GetKeyIDs returns array of IDs of keys in this KeyRing. +// Not supported on go-mobile clients. func (keyRing *KeyRing) GetKeyIDs() []uint64 { var res = make([]uint64, len(keyRing.entities)) for id, e := range keyRing.entities { @@ -187,6 +217,21 @@ func (keyRing *KeyRing) GetKeyIDs() []uint64 { return res } +// GetHexKeyIDsJson returns an IDs of keys in this KeyRing as a json array. +// Key ids are encoded as hexadecimal and nil is returned if an error occurs. +// Helper function for go-mobile clients. +func (keyRing *KeyRing) GetHexKeyIDsJson() []byte { + var res = make([]string, len(keyRing.entities)) + for id, e := range keyRing.entities { + res[id] = keyIDToHex(e.PrimaryKey.KeyId) + } + keyIdsJson, err := json.Marshal(res) + if err != nil { + return nil + } + return keyIdsJson +} + // --- Filter keyrings // FilterExpiredKeys takes a given KeyRing list and it returns only those @@ -194,7 +239,7 @@ func (keyRing *KeyRing) GetKeyIDs() []uint64 { // parts of these KeyRings. func FilterExpiredKeys(contactKeys []*KeyRing) (filteredKeys []*KeyRing, err error) { now := time.Now() - hasExpiredEntity := false //nolint:ifshort + hasExpiredEntity := false filteredKeys = make([]*KeyRing, 0) for _, contactKeyRing := range contactKeys { @@ -204,7 +249,11 @@ func FilterExpiredKeys(contactKeys []*KeyRing) (filteredKeys []*KeyRing, err err hasExpired := false hasUnexpired := false for _, subkey := range entity.Subkeys { - if subkey.PublicKey.KeyExpired(subkey.Sig, now) { + latestValid, err := subkey.LatestValidBindingSignature(now, &packet.Config{}) + if err != nil { + hasExpired = true + } + if subkey.PublicKey.KeyExpired(latestValid, now) { hasExpired = true } else { hasUnexpired = true diff --git a/crypto/keyring_message.go b/crypto/keyring_message.go deleted file mode 100644 index f0fc16e0..00000000 --- a/crypto/keyring_message.go +++ /dev/null @@ -1,355 +0,0 @@ -package crypto - -import ( - "bytes" - "io" - "io/ioutil" - "time" - - "github.com/ProtonMail/go-crypto/openpgp" - "github.com/ProtonMail/go-crypto/openpgp/packet" - "github.com/ProtonMail/gopenpgp/v2/constants" - "github.com/pkg/errors" -) - -// Encrypt encrypts a PlainMessage, outputs a PGPMessage. -// If an unlocked private key is also provided it will also sign the message. -// * message : The plaintext input as a PlainMessage. -// * privateKey : (optional) an unlocked private keyring to include signature in the message. -func (keyRing *KeyRing) Encrypt(message *PlainMessage, privateKey *KeyRing) (*PGPMessage, error) { - return asymmetricEncrypt(message, keyRing, privateKey, false, nil) -} - -// EncryptWithContext encrypts a PlainMessage, outputs a PGPMessage. -// If an unlocked private key is also provided it will also sign the message. -// * message : The plaintext input as a PlainMessage. -// * privateKey : (optional) an unlocked private keyring to include signature in the message. -// * signingContext : (optional) the context for the signature. -func (keyRing *KeyRing) EncryptWithContext(message *PlainMessage, privateKey *KeyRing, signingContext *SigningContext) (*PGPMessage, error) { - return asymmetricEncrypt(message, keyRing, privateKey, false, signingContext) -} - -// EncryptWithCompression encrypts with compression support a PlainMessage to PGPMessage using public/private keys. -// * message : The plain data as a PlainMessage. -// * privateKey : (optional) an unlocked private keyring to include signature in the message. -// * output : The encrypted data as PGPMessage. -func (keyRing *KeyRing) EncryptWithCompression(message *PlainMessage, privateKey *KeyRing) (*PGPMessage, error) { - return asymmetricEncrypt(message, keyRing, privateKey, true, nil) -} - -// EncryptWithContextAndCompression encrypts with compression support a PlainMessage to PGPMessage using public/private keys. -// * message : The plain data as a PlainMessage. -// * privateKey : (optional) an unlocked private keyring to include signature in the message. -// * signingContext : (optional) the context for the signature. -// * output : The encrypted data as PGPMessage. -func (keyRing *KeyRing) EncryptWithContextAndCompression(message *PlainMessage, privateKey *KeyRing, signingContext *SigningContext) (*PGPMessage, error) { - return asymmetricEncrypt(message, keyRing, privateKey, true, signingContext) -} - -// Decrypt decrypts encrypted string using pgp keys, returning a PlainMessage -// * message : The encrypted input as a PGPMessage -// * verifyKey : Public key for signature verification (optional) -// * verifyTime : Time at verification (necessary only if verifyKey is not nil) -// * verificationContext : (optional) the context for the signature verification. -// -// When verifyKey is not provided, then verifyTime should be zero, and -// signature verification will be ignored. -func (keyRing *KeyRing) Decrypt( - message *PGPMessage, verifyKey *KeyRing, verifyTime int64, -) (*PlainMessage, error) { - return asymmetricDecrypt(message.NewReader(), keyRing, verifyKey, verifyTime, nil) -} - -// DecryptWithContext decrypts encrypted string using pgp keys, returning a PlainMessage -// * message : The encrypted input as a PGPMessage -// * verifyKey : Public key for signature verification (optional) -// * verifyTime : Time at verification (necessary only if verifyKey is not nil) -// * verificationContext : (optional) the context for the signature verification. -// -// When verifyKey is not provided, then verifyTime should be zero, and -// signature verification will be ignored. -func (keyRing *KeyRing) DecryptWithContext( - message *PGPMessage, - verifyKey *KeyRing, - verifyTime int64, - verificationContext *VerificationContext, -) (*PlainMessage, error) { - return asymmetricDecrypt(message.NewReader(), keyRing, verifyKey, verifyTime, verificationContext) -} - -// SignDetached generates and returns a PGPSignature for a given PlainMessage. -func (keyRing *KeyRing) SignDetached(message *PlainMessage) (*PGPSignature, error) { - return keyRing.SignDetachedWithContext(message, nil) -} - -// SignDetachedWithContext generates and returns a PGPSignature for a given PlainMessage. -// If a context is provided, it is added to the signature as notation data -// with the name set in `constants.SignatureContextName`. -func (keyRing *KeyRing) SignDetachedWithContext(message *PlainMessage, context *SigningContext) (*PGPSignature, error) { - return signMessageDetached( - keyRing, - message.NewReader(), - message.IsBinary(), - context, - ) -} - -// VerifyDetached verifies a PlainMessage with a detached PGPSignature -// and returns a SignatureVerificationError if fails. -func (keyRing *KeyRing) VerifyDetached(message *PlainMessage, signature *PGPSignature, verifyTime int64) error { - _, err := verifySignature( - keyRing.entities, - message.NewReader(), - signature.GetBinary(), - verifyTime, - nil, - ) - return err -} - -// VerifyDetachedWithContext verifies a PlainMessage with a detached PGPSignature -// and returns a SignatureVerificationError if fails. -// If a context is provided, it verifies that the signature is valid in the given context, using -// the signature notation with name the name set in `constants.SignatureContextName`. -func (keyRing *KeyRing) VerifyDetachedWithContext(message *PlainMessage, signature *PGPSignature, verifyTime int64, verificationContext *VerificationContext) error { - _, err := verifySignature( - keyRing.entities, - message.NewReader(), - signature.GetBinary(), - verifyTime, - verificationContext, - ) - return err -} - -// SignDetachedEncrypted generates and returns a PGPMessage -// containing an encrypted detached signature for a given PlainMessage. -func (keyRing *KeyRing) SignDetachedEncrypted(message *PlainMessage, encryptionKeyRing *KeyRing) (encryptedSignature *PGPMessage, err error) { - if encryptionKeyRing == nil { - return nil, errors.New("gopenpgp: no encryption key ring provided") - } - signature, err := keyRing.SignDetached(message) - if err != nil { - return nil, err - } - plainMessage := NewPlainMessage(signature.GetBinary()) - encryptedSignature, err = encryptionKeyRing.Encrypt(plainMessage, nil) - return -} - -// VerifyDetachedEncrypted verifies a PlainMessage -// with a PGPMessage containing an encrypted detached signature -// and returns a SignatureVerificationError if fails. -func (keyRing *KeyRing) VerifyDetachedEncrypted(message *PlainMessage, encryptedSignature *PGPMessage, decryptionKeyRing *KeyRing, verifyTime int64) error { - if decryptionKeyRing == nil { - return errors.New("gopenpgp: no decryption key ring provided") - } - plainMessage, err := decryptionKeyRing.Decrypt(encryptedSignature, nil, 0) - if err != nil { - return err - } - signature := NewPGPSignature(plainMessage.GetBinary()) - return keyRing.VerifyDetached(message, signature, verifyTime) -} - -// GetVerifiedSignatureTimestamp verifies a PlainMessage with a detached PGPSignature -// returns the creation time of the signature if it succeeds -// and returns a SignatureVerificationError if fails. -func (keyRing *KeyRing) GetVerifiedSignatureTimestamp(message *PlainMessage, signature *PGPSignature, verifyTime int64) (int64, error) { - sigPacket, err := verifySignature( - keyRing.entities, - message.NewReader(), - signature.GetBinary(), - verifyTime, - nil, - ) - if err != nil { - return 0, err - } - return sigPacket.CreationTime.Unix(), nil -} - -// GetVerifiedSignatureTimestampWithContext verifies a PlainMessage with a detached PGPSignature -// returns the creation time of the signature if it succeeds -// and returns a SignatureVerificationError if fails. -// If a context is provided, it verifies that the signature is valid in the given context, using -// the signature notation with name the name set in `constants.SignatureContextName`. -func (keyRing *KeyRing) GetVerifiedSignatureTimestampWithContext( - message *PlainMessage, - signature *PGPSignature, - verifyTime int64, - verificationContext *VerificationContext, -) (int64, error) { - sigPacket, err := verifySignature( - keyRing.entities, - message.NewReader(), - signature.GetBinary(), - verifyTime, - verificationContext, - ) - if err != nil { - return 0, err - } - return sigPacket.CreationTime.Unix(), nil -} - -// ------ INTERNAL FUNCTIONS ------- - -// Core for encryption+signature (non-streaming) functions. -func asymmetricEncrypt( - plainMessage *PlainMessage, - publicKey, privateKey *KeyRing, - compress bool, - signingContext *SigningContext, -) (*PGPMessage, error) { - var outBuf bytes.Buffer - var encryptWriter io.WriteCloser - var err error - - hints := &openpgp.FileHints{ - IsBinary: plainMessage.IsBinary(), - FileName: plainMessage.Filename, - ModTime: plainMessage.getFormattedTime(), - } - - encryptWriter, err = asymmetricEncryptStream(hints, &outBuf, &outBuf, publicKey, privateKey, compress, signingContext) - if err != nil { - return nil, err - } - - _, err = encryptWriter.Write(plainMessage.GetBinary()) - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: error in writing to message") - } - - err = encryptWriter.Close() - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: error in closing message") - } - - return &PGPMessage{outBuf.Bytes()}, nil -} - -// Core for encryption+signature (all) functions. -func asymmetricEncryptStream( - hints *openpgp.FileHints, - keyPacketWriter io.Writer, - dataPacketWriter io.Writer, - publicKey, privateKey *KeyRing, - compress bool, - signingContext *SigningContext, -) (encryptWriter io.WriteCloser, err error) { - config := &packet.Config{ - DefaultCipher: packet.CipherAES256, - Time: getTimeGenerator(), - } - - if compress { - config.DefaultCompressionAlgo = constants.DefaultCompression - config.CompressionConfig = &packet.CompressionConfig{Level: constants.DefaultCompressionLevel} - } - - if signingContext != nil { - config.SignatureNotations = append(config.SignatureNotations, signingContext.getNotation()) - } - - var signEntity *openpgp.Entity - if privateKey != nil && len(privateKey.entities) > 0 { - var err error - signEntity, err = privateKey.getSigningEntity() - if err != nil { - return nil, err - } - } - - if hints.IsBinary { - encryptWriter, err = openpgp.EncryptSplit(keyPacketWriter, dataPacketWriter, publicKey.entities, signEntity, hints, config) - } else { - encryptWriter, err = openpgp.EncryptTextSplit(keyPacketWriter, dataPacketWriter, publicKey.entities, signEntity, hints, config) - } - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: error in encrypting asymmetrically") - } - return encryptWriter, nil -} - -// Core for decryption+verification (non streaming) functions. -func asymmetricDecrypt( - encryptedIO io.Reader, - privateKey *KeyRing, - verifyKey *KeyRing, - verifyTime int64, - verificationContext *VerificationContext, -) (message *PlainMessage, err error) { - messageDetails, err := asymmetricDecryptStream( - encryptedIO, - privateKey, - verifyKey, - verifyTime, - verificationContext, - ) - if err != nil { - return nil, err - } - - body, err := ioutil.ReadAll(messageDetails.UnverifiedBody) - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: error in reading message body") - } - - if verifyKey != nil { - processSignatureExpiration(messageDetails, verifyTime) - err = verifyDetailsSignature(messageDetails, verifyKey, verificationContext) - } - - return &PlainMessage{ - Data: body, - TextType: !messageDetails.LiteralData.IsBinary, - Filename: messageDetails.LiteralData.FileName, - Time: messageDetails.LiteralData.Time, - }, err -} - -// Core for decryption+verification (all) functions. -func asymmetricDecryptStream( - encryptedIO io.Reader, - privateKey *KeyRing, - verifyKey *KeyRing, - verifyTime int64, - verificationContext *VerificationContext, -) (messageDetails *openpgp.MessageDetails, err error) { - privKeyEntries := privateKey.entities - var additionalEntries openpgp.EntityList - - if verifyKey != nil { - additionalEntries = verifyKey.entities - } - - if additionalEntries != nil { - privKeyEntries = append(privKeyEntries, additionalEntries...) - } - - config := &packet.Config{ - Time: func() time.Time { - if verifyTime == 0 { - /* - We default to current time while decrypting and verifying - but the caller will remove signature expiration errors later on. - See processSignatureExpiration(). - */ - return getNow() - } - return time.Unix(verifyTime, 0) - }, - } - - if verificationContext != nil { - config.KnownNotations = map[string]bool{constants.SignatureContextName: true} - } - - messageDetails, err = openpgp.ReadMessage(encryptedIO, privKeyEntries, nil, config) - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: error in reading message") - } - return messageDetails, err -} diff --git a/crypto/keyring_message_test.go b/crypto/keyring_message_test.go deleted file mode 100644 index de1f8eeb..00000000 --- a/crypto/keyring_message_test.go +++ /dev/null @@ -1,80 +0,0 @@ -package crypto - -import ( - "io/ioutil" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestAEADKeyRingDecryption(t *testing.T) { - pgpMessageData, err := ioutil.ReadFile("testdata/gpg2.3-aead-pgp-message.pgp") - if err != nil { - t.Fatal("Expected no error when reading message data, got:", err) - } - pgpMessage := NewPGPMessage(pgpMessageData) - - aeadKey, err := NewKeyFromArmored(readTestFile("gpg2.3-aead-test-key.asc", false)) - if err != nil { - t.Fatal("Expected no error when unarmoring key, got:", err) - } - - aeadKeyUnlocked, err := aeadKey.Unlock([]byte("test")) - if err != nil { - t.Fatal("Expected no error when unlocking, got:", err) - } - kR, err := NewKeyRing(aeadKeyUnlocked) - if err != nil { - t.Fatal("Expected no error when creating the keyring, got:", err) - } - defer kR.ClearPrivateParams() - - decrypted, err := kR.Decrypt(pgpMessage, nil, 0) - if err != nil { - t.Fatal("Expected no error when decrypting, got:", err) - } - - assert.Exactly(t, "hello world\n", decrypted.GetString()) -} - -func TestTextMessageEncryptionWithSignatureAndContext(t *testing.T) { - var message = NewPlainMessageFromString("plain text") - var testContext = "test-context" - - ciphertext, err := keyRingTestPublic.EncryptWithContext(message, keyRingTestPrivate, NewSigningContext(testContext, true)) - if err != nil { - t.Fatal("Expected no error when encrypting, got:", err) - } - - decrypted, err := keyRingTestPrivate.DecryptWithContext( - ciphertext, - keyRingTestPublic, - GetUnixTime(), - NewVerificationContext(testContext, true, 0), - ) - if err != nil { - t.Fatal("Expected no error when decrypting, got:", err) - } - assert.Exactly(t, message.GetString(), decrypted.GetString()) -} - -func TestTextMessageEncryptionWithSignatureAndContextAndCompression(t *testing.T) { - var message = NewPlainMessageFromString("plain text") - var testContext = "test-context" - - ciphertext, err := keyRingTestPublic.EncryptWithContextAndCompression(message, keyRingTestPrivate, NewSigningContext(testContext, true)) - if err != nil { - t.Fatal("Expected no error when encrypting, got:", err) - } - - decrypted, err := keyRingTestPrivate.DecryptWithContext( - ciphertext, - keyRingTestPublic, - GetUnixTime(), - NewVerificationContext(testContext, true, 0), - ) - if err != nil { - t.Fatal("Expected no error when decrypting, got:", err) - } - assert.Exactly(t, message.GetString(), decrypted.GetString()) -} diff --git a/crypto/keyring_session.go b/crypto/keyring_session.go deleted file mode 100644 index 638c1f78..00000000 --- a/crypto/keyring_session.go +++ /dev/null @@ -1,103 +0,0 @@ -package crypto - -import ( - "bytes" - "strconv" - - "github.com/pkg/errors" - - "github.com/ProtonMail/go-crypto/openpgp/packet" -) - -// DecryptSessionKey returns the decrypted session key from one or multiple binary encrypted session key packets. -func (keyRing *KeyRing) DecryptSessionKey(keyPacket []byte) (*SessionKey, error) { - var p packet.Packet - var ek *packet.EncryptedKey - - var err error - var hasPacket = false - var decryptErr error - - keyReader := bytes.NewReader(keyPacket) - packets := packet.NewReader(keyReader) - -Loop: - for { - if p, err = packets.Next(); err != nil { - break - } - - switch p := p.(type) { - case *packet.EncryptedKey: - hasPacket = true - ek = p - - for _, key := range keyRing.entities.DecryptionKeys() { - priv := key.PrivateKey - if priv.Encrypted { - continue - } - - if decryptErr = ek.Decrypt(priv, nil); decryptErr == nil { - break Loop - } - } - - case *packet.SymmetricallyEncrypted, - *packet.AEADEncrypted, - *packet.Compressed, - *packet.LiteralData: - break Loop - - default: - continue Loop - } - } - - if !hasPacket { - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: couldn't find a session key packet") - } else { - return nil, errors.New("gopenpgp: couldn't find a session key packet") - } - } - - if decryptErr != nil { - return nil, errors.Wrap(decryptErr, "gopenpgp: error in decrypting") - } - - if ek == nil || ek.Key == nil { - return nil, errors.New("gopenpgp: unable to decrypt session key: no valid decryption key") - } - - return newSessionKeyFromEncrypted(ek) -} - -// EncryptSessionKey encrypts the session key with the unarmored -// publicKey and returns a binary public-key encrypted session key packet. -func (keyRing *KeyRing) EncryptSessionKey(sk *SessionKey) ([]byte, error) { - outbuf := &bytes.Buffer{} - cf, err := sk.GetCipherFunc() - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: unable to encrypt session key") - } - - pubKeys := make([]*packet.PublicKey, 0, len(keyRing.entities)) - for _, e := range keyRing.entities { - encryptionKey, ok := e.EncryptionKey(getNow()) - if !ok { - return nil, errors.New("gopenpgp: encryption key is unavailable for key id " + strconv.FormatUint(e.PrimaryKey.KeyId, 16)) - } - pubKeys = append(pubKeys, encryptionKey.PublicKey) - } - if len(pubKeys) == 0 { - return nil, errors.New("cannot set key: no public key available") - } - - for _, pub := range pubKeys { - if err := packet.SerializeEncryptedKey(outbuf, pub, cf, sk.Key, nil); err != nil { - return nil, errors.Wrap(err, "gopenpgp: cannot set key") - } - } - return outbuf.Bytes(), nil -} diff --git a/crypto/keyring_streaming.go b/crypto/keyring_streaming.go deleted file mode 100644 index 6d99402d..00000000 --- a/crypto/keyring_streaming.go +++ /dev/null @@ -1,539 +0,0 @@ -package crypto - -import ( - "bytes" - "io" - "time" - - "github.com/ProtonMail/go-crypto/openpgp" - "github.com/pkg/errors" -) - -type Reader interface { - Read(b []byte) (n int, err error) -} - -type Writer interface { - Write(b []byte) (n int, err error) -} - -type WriteCloser interface { - Write(b []byte) (n int, err error) - Close() (err error) -} - -type PlainMessageMetadata struct { - IsBinary bool - Filename string - ModTime int64 -} - -func NewPlainMessageMetadata(isBinary bool, filename string, modTime int64) *PlainMessageMetadata { - return &PlainMessageMetadata{IsBinary: isBinary, Filename: filename, ModTime: modTime} -} - -// EncryptStream is used to encrypt data as a Writer. -// It takes a writer for the encrypted data and returns a WriteCloser for the plaintext data -// If signKeyRing is not nil, it is used to do an embedded signature. -func (keyRing *KeyRing) EncryptStream( - pgpMessageWriter Writer, - plainMessageMetadata *PlainMessageMetadata, - signKeyRing *KeyRing, -) (plainMessageWriter WriteCloser, err error) { - return encryptStream( - keyRing, - pgpMessageWriter, - pgpMessageWriter, - plainMessageMetadata, - signKeyRing, - false, - nil, - ) -} - -// EncryptStreamWithContext is used to encrypt data as a Writer. -// It takes a writer for the encrypted data and returns a WriteCloser for the plaintext data -// If signKeyRing is not nil, it is used to do an embedded signature. -// * signingContext : (optional) a context for the embedded signature. -func (keyRing *KeyRing) EncryptStreamWithContext( - pgpMessageWriter Writer, - plainMessageMetadata *PlainMessageMetadata, - signKeyRing *KeyRing, - signingContext *SigningContext, -) (plainMessageWriter WriteCloser, err error) { - return encryptStream( - keyRing, - pgpMessageWriter, - pgpMessageWriter, - plainMessageMetadata, - signKeyRing, - false, - signingContext, - ) -} - -// EncryptStreamWithCompression is used to encrypt data as a Writer. -// The plaintext data is compressed before being encrypted. -// It takes a writer for the encrypted data and returns a WriteCloser for the plaintext data -// If signKeyRing is not nil, it is used to do an embedded signature. -func (keyRing *KeyRing) EncryptStreamWithCompression( - pgpMessageWriter Writer, - plainMessageMetadata *PlainMessageMetadata, - signKeyRing *KeyRing, -) (plainMessageWriter WriteCloser, err error) { - return encryptStream( - keyRing, - pgpMessageWriter, - pgpMessageWriter, - plainMessageMetadata, - signKeyRing, - true, - nil, - ) -} - -// EncryptStreamWithContextAndCompression is used to encrypt data as a Writer. -// The plaintext data is compressed before being encrypted. -// It takes a writer for the encrypted data and returns a WriteCloser for the plaintext data -// If signKeyRing is not nil, it is used to do an embedded signature. -// * signingContext : (optional) a context for the embedded signature. -func (keyRing *KeyRing) EncryptStreamWithContextAndCompression( - pgpMessageWriter Writer, - plainMessageMetadata *PlainMessageMetadata, - signKeyRing *KeyRing, - signingContext *SigningContext, -) (plainMessageWriter WriteCloser, err error) { - return encryptStream( - keyRing, - pgpMessageWriter, - pgpMessageWriter, - plainMessageMetadata, - signKeyRing, - true, - signingContext, - ) -} - -func encryptStream( - encryptionKeyRing *KeyRing, - keyPacketWriter Writer, - dataPacketWriter Writer, - plainMessageMetadata *PlainMessageMetadata, - signKeyRing *KeyRing, - compress bool, - signingContext *SigningContext, -) (plainMessageWriter WriteCloser, err error) { - if plainMessageMetadata == nil { - // Use sensible default metadata - plainMessageMetadata = &PlainMessageMetadata{ - IsBinary: true, - Filename: "", - ModTime: GetUnixTime(), - } - } - - hints := &openpgp.FileHints{ - FileName: plainMessageMetadata.Filename, - IsBinary: plainMessageMetadata.IsBinary, - ModTime: time.Unix(plainMessageMetadata.ModTime, 0), - } - - plainMessageWriter, err = asymmetricEncryptStream(hints, keyPacketWriter, dataPacketWriter, encryptionKeyRing, signKeyRing, compress, signingContext) - if err != nil { - return nil, err - } - return plainMessageWriter, nil -} - -// EncryptSplitResult is used to wrap the encryption writecloser while storing the key packet. -type EncryptSplitResult struct { - isClosed bool - keyPacketBuf *bytes.Buffer - keyPacket []byte - plainMessageWriter WriteCloser // The writer to writer plaintext data in. -} - -func (res *EncryptSplitResult) Write(b []byte) (n int, err error) { - return res.plainMessageWriter.Write(b) -} - -func (res *EncryptSplitResult) Close() (err error) { - err = res.plainMessageWriter.Close() - if err != nil { - return err - } - res.isClosed = true - res.keyPacket = res.keyPacketBuf.Bytes() - return nil -} - -// GetKeyPacket returns the Public-Key Encrypted Session Key Packets (https://datatracker.ietf.org/doc/html/rfc4880#section-5.1). -// This can be retrieved only after the message has been fully written and the writer is closed. -func (res *EncryptSplitResult) GetKeyPacket() (keyPacket []byte, err error) { - if !res.isClosed { - return nil, errors.New("gopenpgp: can't access key packet until the message writer has been closed") - } - return res.keyPacket, nil -} - -// EncryptSplitStream is used to encrypt data as a stream. -// It takes a writer for the Symmetrically Encrypted Data Packet -// (https://datatracker.ietf.org/doc/html/rfc4880#section-5.7) -// and returns a writer for the plaintext data and the key packet. -// If signKeyRing is not nil, it is used to do an embedded signature. -func (keyRing *KeyRing) EncryptSplitStream( - dataPacketWriter Writer, - plainMessageMetadata *PlainMessageMetadata, - signKeyRing *KeyRing, -) (*EncryptSplitResult, error) { - return encryptSplitStream( - keyRing, - dataPacketWriter, - plainMessageMetadata, - signKeyRing, - false, - nil, - ) -} - -// EncryptSplitStreamWithContext is used to encrypt data as a stream. -// It takes a writer for the Symmetrically Encrypted Data Packet -// (https://datatracker.ietf.org/doc/html/rfc4880#section-5.7) -// and returns a writer for the plaintext data and the key packet. -// If signKeyRing is not nil, it is used to do an embedded signature. -// * signingContext : (optional) a context for the embedded signature. -func (keyRing *KeyRing) EncryptSplitStreamWithContext( - dataPacketWriter Writer, - plainMessageMetadata *PlainMessageMetadata, - signKeyRing *KeyRing, - signingContext *SigningContext, -) (*EncryptSplitResult, error) { - return encryptSplitStream( - keyRing, - dataPacketWriter, - plainMessageMetadata, - signKeyRing, - false, - signingContext, - ) -} - -// EncryptSplitStreamWithCompression is used to encrypt data as a stream. -// It takes a writer for the Symmetrically Encrypted Data Packet -// (https://datatracker.ietf.org/doc/html/rfc4880#section-5.7) -// and returns a writer for the plaintext data and the key packet. -// If signKeyRing is not nil, it is used to do an embedded signature. -func (keyRing *KeyRing) EncryptSplitStreamWithCompression( - dataPacketWriter Writer, - plainMessageMetadata *PlainMessageMetadata, - signKeyRing *KeyRing, -) (*EncryptSplitResult, error) { - return encryptSplitStream( - keyRing, - dataPacketWriter, - plainMessageMetadata, - signKeyRing, - true, - nil, - ) -} - -// EncryptSplitStreamWithContextAndCompression is used to encrypt data as a stream. -// It takes a writer for the Symmetrically Encrypted Data Packet -// (https://datatracker.ietf.org/doc/html/rfc4880#section-5.7) -// and returns a writer for the plaintext data and the key packet. -// If signKeyRing is not nil, it is used to do an embedded signature. -// * signingContext : (optional) a context for the embedded signature. -func (keyRing *KeyRing) EncryptSplitStreamWithContextAndCompression( - dataPacketWriter Writer, - plainMessageMetadata *PlainMessageMetadata, - signKeyRing *KeyRing, - signingContext *SigningContext, -) (*EncryptSplitResult, error) { - return encryptSplitStream( - keyRing, - dataPacketWriter, - plainMessageMetadata, - signKeyRing, - true, - signingContext, - ) -} - -func encryptSplitStream( - encryptionKeyRing *KeyRing, - dataPacketWriter Writer, - plainMessageMetadata *PlainMessageMetadata, - signKeyRing *KeyRing, - compress bool, - signingContext *SigningContext, -) (*EncryptSplitResult, error) { - var keyPacketBuf bytes.Buffer - plainMessageWriter, err := encryptStream( - encryptionKeyRing, - &keyPacketBuf, - dataPacketWriter, - plainMessageMetadata, - signKeyRing, - compress, - signingContext, - ) - if err != nil { - return nil, err - } - - return &EncryptSplitResult{ - keyPacketBuf: &keyPacketBuf, - plainMessageWriter: plainMessageWriter, - }, nil -} - -// PlainMessageReader is used to wrap the data of the decrypted plain message. -// It can be used to read the decrypted data and verify the embedded signature. -type PlainMessageReader struct { - details *openpgp.MessageDetails - verifyKeyRing *KeyRing - verifyTime int64 - readAll bool - verificationContext *VerificationContext -} - -// GetMetadata returns the metadata of the decrypted message. -func (msg *PlainMessageReader) GetMetadata() *PlainMessageMetadata { - return &PlainMessageMetadata{ - Filename: msg.details.LiteralData.FileName, - IsBinary: msg.details.LiteralData.IsBinary, - ModTime: int64(msg.details.LiteralData.Time), - } -} - -// Read is used to access the message decrypted data. -// Makes PlainMessageReader implement the Reader interface. -func (msg *PlainMessageReader) Read(b []byte) (n int, err error) { - n, err = msg.details.UnverifiedBody.Read(b) - if errors.Is(err, io.EOF) { - msg.readAll = true - } - return -} - -// VerifySignature is used to verify that the signature is valid. -// This method needs to be called once all the data has been read. -// It will return an error if the signature is invalid -// or if the message hasn't been read entirely. -func (msg *PlainMessageReader) VerifySignature() (err error) { - if !msg.readAll { - return errors.New("gopenpgp: can't verify the signature until the message reader has been read entirely") - } - if msg.verifyKeyRing != nil { - processSignatureExpiration(msg.details, msg.verifyTime) - err = verifyDetailsSignature(msg.details, msg.verifyKeyRing, msg.verificationContext) - } else { - err = errors.New("gopenpgp: no verify keyring was provided before decryption") - } - return -} - -// DecryptStream is used to decrypt a pgp message as a Reader. -// It takes a reader for the message data -// and returns a PlainMessageReader for the plaintext data. -// If verifyKeyRing is not nil, PlainMessageReader.VerifySignature() will -// verify the embedded signature with the given key ring and verification time. -func (keyRing *KeyRing) DecryptStream( - message Reader, - verifyKeyRing *KeyRing, - verifyTime int64, -) (plainMessage *PlainMessageReader, err error) { - return decryptStream( - keyRing, - message, - verifyKeyRing, - verifyTime, - nil, - ) -} - -// DecryptStreamWithContext is used to decrypt a pgp message as a Reader. -// It takes a reader for the message data -// and returns a PlainMessageReader for the plaintext data. -// If verifyKeyRing is not nil, PlainMessageReader.VerifySignature() will -// verify the embedded signature with the given key ring and verification time. -// * verificationContext (optional): context for the signature verification. -func (keyRing *KeyRing) DecryptStreamWithContext( - message Reader, - verifyKeyRing *KeyRing, - verifyTime int64, - verificationContext *VerificationContext, -) (plainMessage *PlainMessageReader, err error) { - return decryptStream( - keyRing, - message, - verifyKeyRing, - verifyTime, - verificationContext, - ) -} - -func decryptStream( - decryptionKeyRing *KeyRing, - message Reader, - verifyKeyRing *KeyRing, - verifyTime int64, - verificationContext *VerificationContext, -) (plainMessage *PlainMessageReader, err error) { - messageDetails, err := asymmetricDecryptStream( - message, - decryptionKeyRing, - verifyKeyRing, - verifyTime, - verificationContext, - ) - if err != nil { - return nil, err - } - - return &PlainMessageReader{ - messageDetails, - verifyKeyRing, - verifyTime, - false, - verificationContext, - }, err -} - -// DecryptSplitStream is used to decrypt a split pgp message as a Reader. -// It takes a key packet and a reader for the data packet -// and returns a PlainMessageReader for the plaintext data. -// If verifyKeyRing is not nil, PlainMessageReader.VerifySignature() will -// verify the embedded signature with the given key ring and verification time. -func (keyRing *KeyRing) DecryptSplitStream( - keypacket []byte, - dataPacketReader Reader, - verifyKeyRing *KeyRing, verifyTime int64, -) (plainMessage *PlainMessageReader, err error) { - messageReader := io.MultiReader( - bytes.NewReader(keypacket), - dataPacketReader, - ) - return keyRing.DecryptStream( - messageReader, - verifyKeyRing, - verifyTime, - ) -} - -// DecryptSplitStreamWithContext is used to decrypt a split pgp message as a Reader. -// It takes a key packet and a reader for the data packet -// and returns a PlainMessageReader for the plaintext data. -// If verifyKeyRing is not nil, PlainMessageReader.VerifySignature() will -// verify the embedded signature with the given key ring and verification time. -// * verificationContext (optional): context for the signature verification. -func (keyRing *KeyRing) DecryptSplitStreamWithContext( - keypacket []byte, - dataPacketReader Reader, - verifyKeyRing *KeyRing, verifyTime int64, - verificationContext *VerificationContext, -) (plainMessage *PlainMessageReader, err error) { - messageReader := io.MultiReader( - bytes.NewReader(keypacket), - dataPacketReader, - ) - return keyRing.DecryptStreamWithContext( - messageReader, - verifyKeyRing, - verifyTime, - verificationContext, - ) -} - -// SignDetachedStream generates and returns a PGPSignature for a given message Reader. -func (keyRing *KeyRing) SignDetachedStream(message Reader) (*PGPSignature, error) { - return keyRing.SignDetachedStreamWithContext(message, nil) -} - -// SignDetachedStreamWithContext generates and returns a PGPSignature for a given message Reader. -// If a context is provided, it is added to the signature as notation data -// with the name set in `constants.SignatureContextName`. -func (keyRing *KeyRing) SignDetachedStreamWithContext(message Reader, context *SigningContext) (*PGPSignature, error) { - return signMessageDetached( - keyRing, - message, - true, - context, - ) -} - -// VerifyDetachedStream verifies a message reader with a detached PGPSignature -// and returns a SignatureVerificationError if fails. -func (keyRing *KeyRing) VerifyDetachedStream( - message Reader, - signature *PGPSignature, - verifyTime int64, -) error { - _, err := verifySignature( - keyRing.entities, - message, - signature.GetBinary(), - verifyTime, - nil, - ) - return err -} - -// VerifyDetachedStreamWithContext verifies a message reader with a detached PGPSignature -// and returns a SignatureVerificationError if fails. -// If a context is provided, it verifies that the signature is valid in the given context, using -// the signature notations. -func (keyRing *KeyRing) VerifyDetachedStreamWithContext( - message Reader, - signature *PGPSignature, - verifyTime int64, - verificationContext *VerificationContext, -) error { - _, err := verifySignature( - keyRing.entities, - message, - signature.GetBinary(), - verifyTime, - verificationContext, - ) - return err -} - -// SignDetachedEncryptedStream generates and returns a PGPMessage -// containing an encrypted detached signature for a given message Reader. -func (keyRing *KeyRing) SignDetachedEncryptedStream( - message Reader, - encryptionKeyRing *KeyRing, -) (encryptedSignature *PGPMessage, err error) { - if encryptionKeyRing == nil { - return nil, errors.New("gopenpgp: no encryption key ring provided") - } - signature, err := keyRing.SignDetachedStream(message) - if err != nil { - return nil, err - } - plainMessage := NewPlainMessage(signature.GetBinary()) - encryptedSignature, err = encryptionKeyRing.Encrypt(plainMessage, nil) - return -} - -// VerifyDetachedEncryptedStream verifies a PlainMessage -// with a PGPMessage containing an encrypted detached signature -// and returns a SignatureVerificationError if fails. -func (keyRing *KeyRing) VerifyDetachedEncryptedStream( - message Reader, - encryptedSignature *PGPMessage, - decryptionKeyRing *KeyRing, - verifyTime int64, -) error { - if decryptionKeyRing == nil { - return errors.New("gopenpgp: no decryption key ring provided") - } - plainMessage, err := decryptionKeyRing.Decrypt(encryptedSignature, nil, 0) - if err != nil { - return err - } - signature := NewPGPSignature(plainMessage.GetBinary()) - return keyRing.VerifyDetachedStream(message, signature, verifyTime) -} diff --git a/crypto/keyring_streaming_test.go b/crypto/keyring_streaming_test.go deleted file mode 100644 index 1cd6e44f..00000000 --- a/crypto/keyring_streaming_test.go +++ /dev/null @@ -1,794 +0,0 @@ -package crypto - -import ( - "bytes" - "io" - "io/ioutil" - "reflect" - "testing" - - "github.com/pkg/errors" -) - -const testContext = "test-context" - -var testMeta = &PlainMessageMetadata{ - IsBinary: true, - Filename: "filename.txt", - ModTime: GetUnixTime(), -} - -func TestKeyRing_EncryptDecryptStream(t *testing.T) { - messageBytes := []byte("Hello World!") - messageReader := bytes.NewReader(messageBytes) - var ciphertextBuf bytes.Buffer - messageWriter, err := keyRingTestPublic.EncryptStream( - &ciphertextBuf, - testMeta, - keyRingTestPrivate, - ) - if err != nil { - t.Fatal("Expected no error while encrypting stream with key ring, got:", err) - } - reachedEnd := false - bufferSize := 2 - buffer := make([]byte, bufferSize) - for !reachedEnd { - n, err := messageReader.Read(buffer) - if err != nil { - if errors.Is(err, io.EOF) { - reachedEnd = true - } else { - t.Fatal("Expected no error while reading data, got:", err) - } - } - writtenTotal := 0 - for writtenTotal < n { - written, err := messageWriter.Write(buffer[writtenTotal:n]) - if err != nil { - t.Fatal("Expected no error while writing data, got:", err) - } - writtenTotal += written - } - } - err = messageWriter.Close() - if err != nil { - t.Fatal("Expected no error while closing plaintext writer, got:", err) - } - ciphertextBytes := ciphertextBuf.Bytes() - decryptedReader, err := keyRingTestPrivate.DecryptStream( - bytes.NewReader(ciphertextBytes), - keyRingTestPublic, - GetUnixTime(), - ) - if err != nil { - t.Fatal("Expected no error while calling decrypting stream with key ring, got:", err) - } - err = decryptedReader.VerifySignature() - if err == nil { - t.Fatal("Expected an error while verifying the signature before reading the data, got nil") - } - decryptedBytes, err := ioutil.ReadAll(decryptedReader) - if err != nil { - t.Fatal("Expected no error while reading the decrypted data, got:", err) - } - if !bytes.Equal(decryptedBytes, messageBytes) { - t.Fatalf("Expected the decrypted data to be %s got %s", string(decryptedBytes), string(messageBytes)) - } - err = decryptedReader.VerifySignature() - if err != nil { - t.Fatal("Expected no error while verifying the signature, got:", err) - } - decryptedMeta := decryptedReader.GetMetadata() - if !reflect.DeepEqual(testMeta, decryptedMeta) { - t.Fatalf("Expected the decrypted metadata to be %v got %v", testMeta, decryptedMeta) - } - decryptedReaderNoVerify, err := keyRingTestPrivate.DecryptStream( - bytes.NewReader(ciphertextBytes), - nil, - 0, - ) - if err != nil { - t.Fatal("Expected no error while calling decrypting stream with key ring, got:", err) - } - decryptedBytes, err = ioutil.ReadAll(decryptedReaderNoVerify) - if err != nil { - t.Fatal("Expected no error while reading the decrypted data, got:", err) - } - if !bytes.Equal(decryptedBytes, messageBytes) { - t.Fatalf("Expected the decrypted data to be %s got %s", string(decryptedBytes), string(messageBytes)) - } - decryptedMeta = decryptedReaderNoVerify.GetMetadata() - if !reflect.DeepEqual(testMeta, decryptedMeta) { - t.Fatalf("Expected the decrypted metadata to be %v got %v", testMeta, decryptedMeta) - } - err = decryptedReaderNoVerify.VerifySignature() - if err == nil { - t.Fatal("Expected an error while verifying the signature with no keyring, got nil") - } -} - -func TestKeyRing_EncryptDecryptStreamWithContext(t *testing.T) { - messageBytes := []byte("Hello World!") - messageReader := bytes.NewReader(messageBytes) - var ciphertextBuf bytes.Buffer - messageWriter, err := keyRingTestPublic.EncryptStreamWithContext( - &ciphertextBuf, - testMeta, - keyRingTestPrivate, - NewSigningContext(testContext, true), - ) - if err != nil { - t.Fatal("Expected no error while encrypting stream with key ring, got:", err) - } - reachedEnd := false - bufferSize := 2 - buffer := make([]byte, bufferSize) - for !reachedEnd { - n, err := messageReader.Read(buffer) - if err != nil { - if errors.Is(err, io.EOF) { - reachedEnd = true - } else { - t.Fatal("Expected no error while reading data, got:", err) - } - } - writtenTotal := 0 - for writtenTotal < n { - written, err := messageWriter.Write(buffer[writtenTotal:n]) - if err != nil { - t.Fatal("Expected no error while writing data, got:", err) - } - writtenTotal += written - } - } - err = messageWriter.Close() - if err != nil { - t.Fatal("Expected no error while closing plaintext writer, got:", err) - } - ciphertextBytes := ciphertextBuf.Bytes() - decryptedReader, err := keyRingTestPrivate.DecryptStreamWithContext( - bytes.NewReader(ciphertextBytes), - keyRingTestPublic, - GetUnixTime(), - NewVerificationContext(testContext, true, 0), - ) - if err != nil { - t.Fatal("Expected no error while calling decrypting stream with key ring, got:", err) - } - err = decryptedReader.VerifySignature() - if err == nil { - t.Fatal("Expected an error while verifying the signature before reading the data, got nil") - } - decryptedBytes, err := ioutil.ReadAll(decryptedReader) - if err != nil { - t.Fatal("Expected no error while reading the decrypted data, got:", err) - } - if !bytes.Equal(decryptedBytes, messageBytes) { - t.Fatalf("Expected the decrypted data to be %s got %s", string(decryptedBytes), string(messageBytes)) - } - err = decryptedReader.VerifySignature() - if err != nil { - t.Fatal("Expected no error while verifying the signature, got:", err) - } - decryptedMeta := decryptedReader.GetMetadata() - if !reflect.DeepEqual(testMeta, decryptedMeta) { - t.Fatalf("Expected the decrypted metadata to be %v got %v", testMeta, decryptedMeta) - } - decryptedReaderNoVerify, err := keyRingTestPrivate.DecryptStream( - bytes.NewReader(ciphertextBytes), - nil, - 0, - ) - if err != nil { - t.Fatal("Expected no error while calling decrypting stream with key ring, got:", err) - } - decryptedBytes, err = ioutil.ReadAll(decryptedReaderNoVerify) - if err != nil { - t.Fatal("Expected no error while reading the decrypted data, got:", err) - } - if !bytes.Equal(decryptedBytes, messageBytes) { - t.Fatalf("Expected the decrypted data to be %s got %s", string(decryptedBytes), string(messageBytes)) - } - decryptedMeta = decryptedReaderNoVerify.GetMetadata() - if !reflect.DeepEqual(testMeta, decryptedMeta) { - t.Fatalf("Expected the decrypted metadata to be %v got %v", testMeta, decryptedMeta) - } - err = decryptedReaderNoVerify.VerifySignature() - if err == nil { - t.Fatal("Expected an error while verifying the signature with no keyring, got nil") - } -} - -func TestKeyRing_EncryptDecryptStreamWithContextAndCompression(t *testing.T) { - messageBytes := []byte("Hello World!") - messageReader := bytes.NewReader(messageBytes) - var ciphertextBuf bytes.Buffer - messageWriter, err := keyRingTestPublic.EncryptStreamWithContextAndCompression( - &ciphertextBuf, - testMeta, - keyRingTestPrivate, - NewSigningContext(testContext, true), - ) - if err != nil { - t.Fatal("Expected no error while encrypting stream with key ring, got:", err) - } - reachedEnd := false - bufferSize := 2 - buffer := make([]byte, bufferSize) - for !reachedEnd { - n, err := messageReader.Read(buffer) - if err != nil { - if errors.Is(err, io.EOF) { - reachedEnd = true - } else { - t.Fatal("Expected no error while reading data, got:", err) - } - } - writtenTotal := 0 - for writtenTotal < n { - written, err := messageWriter.Write(buffer[writtenTotal:n]) - if err != nil { - t.Fatal("Expected no error while writing data, got:", err) - } - writtenTotal += written - } - } - err = messageWriter.Close() - if err != nil { - t.Fatal("Expected no error while closing plaintext writer, got:", err) - } - ciphertextBytes := ciphertextBuf.Bytes() - decryptedReader, err := keyRingTestPrivate.DecryptStreamWithContext( - bytes.NewReader(ciphertextBytes), - keyRingTestPublic, - GetUnixTime(), - NewVerificationContext(testContext, true, 0), - ) - if err != nil { - t.Fatal("Expected no error while calling decrypting stream with key ring, got:", err) - } - err = decryptedReader.VerifySignature() - if err == nil { - t.Fatal("Expected an error while verifying the signature before reading the data, got nil") - } - decryptedBytes, err := ioutil.ReadAll(decryptedReader) - if err != nil { - t.Fatal("Expected no error while reading the decrypted data, got:", err) - } - if !bytes.Equal(decryptedBytes, messageBytes) { - t.Fatalf("Expected the decrypted data to be %s got %s", string(decryptedBytes), string(messageBytes)) - } - err = decryptedReader.VerifySignature() - if err != nil { - t.Fatal("Expected no error while verifying the signature, got:", err) - } - decryptedMeta := decryptedReader.GetMetadata() - if !reflect.DeepEqual(testMeta, decryptedMeta) { - t.Fatalf("Expected the decrypted metadata to be %v got %v", testMeta, decryptedMeta) - } - decryptedReaderNoVerify, err := keyRingTestPrivate.DecryptStream( - bytes.NewReader(ciphertextBytes), - nil, - 0, - ) - if err != nil { - t.Fatal("Expected no error while calling decrypting stream with key ring, got:", err) - } - decryptedBytes, err = ioutil.ReadAll(decryptedReaderNoVerify) - if err != nil { - t.Fatal("Expected no error while reading the decrypted data, got:", err) - } - if !bytes.Equal(decryptedBytes, messageBytes) { - t.Fatalf("Expected the decrypted data to be %s got %s", string(decryptedBytes), string(messageBytes)) - } - decryptedMeta = decryptedReaderNoVerify.GetMetadata() - if !reflect.DeepEqual(testMeta, decryptedMeta) { - t.Fatalf("Expected the decrypted metadata to be %v got %v", testMeta, decryptedMeta) - } - err = decryptedReaderNoVerify.VerifySignature() - if err == nil { - t.Fatal("Expected an error while verifying the signature with no keyring, got nil") - } -} - -func TestKeyRing_EncryptStreamCompatible(t *testing.T) { - enc := func(w io.Writer, meta *PlainMessageMetadata, kr *KeyRing) (io.WriteCloser, error) { - return keyRingTestPublic.EncryptStream( - w, - meta, - kr, - ) - } - testKeyRing_EncryptStreamCompatible(enc, t) -} - -func TestKeyRing_EncryptStreamWithCompressionCompatible(t *testing.T) { - enc := func(w io.Writer, meta *PlainMessageMetadata, kr *KeyRing) (io.WriteCloser, error) { - return keyRingTestPublic.EncryptStreamWithCompression( - w, - meta, - kr, - ) - } - testKeyRing_EncryptStreamCompatible(enc, t) -} - -type keyringEncryptionFunction = func(io.Writer, *PlainMessageMetadata, *KeyRing) (io.WriteCloser, error) - -func testKeyRing_EncryptStreamCompatible(encrypt keyringEncryptionFunction, t *testing.T) { - messageBytes := []byte("Hello World!") - messageReader := bytes.NewReader(messageBytes) - var ciphertextBuf bytes.Buffer - messageWriter, err := encrypt( - &ciphertextBuf, - testMeta, - keyRingTestPrivate, - ) - if err != nil { - t.Fatal("Expected no error while encrypting stream with key ring, got:", err) - } - reachedEnd := false - bufferSize := 2 - buffer := make([]byte, bufferSize) - for !reachedEnd { - n, err := messageReader.Read(buffer) - if err != nil { - if errors.Is(err, io.EOF) { - reachedEnd = true - } else { - t.Fatal("Expected no error while reading data, got:", err) - } - } - writtenTotal := 0 - for writtenTotal < n { - written, err := messageWriter.Write(buffer[writtenTotal:n]) - if err != nil { - t.Fatal("Expected no error while writing data, got:", err) - } - writtenTotal += written - } - } - err = messageWriter.Close() - if err != nil { - t.Fatal("Expected no error while closing plaintext writer, got:", err) - } - encryptedData := ciphertextBuf.Bytes() - decryptedMsg, err := keyRingTestPrivate.Decrypt( - NewPGPMessage(encryptedData), - keyRingTestPublic, - GetUnixTime(), - ) - if err != nil { - t.Fatal("Expected no error while calling normal decrypt with key ring, got:", err) - } - decryptedBytes := decryptedMsg.GetBinary() - if !bytes.Equal(decryptedBytes, messageBytes) { - t.Fatalf("Expected the normally decrypted data to be %s got %s", string(decryptedBytes), string(messageBytes)) - } - if testMeta.IsBinary != decryptedMsg.IsBinary() { - t.Fatalf("Expected isBinary to be %t got %t", testMeta.IsBinary, decryptedMsg.IsBinary()) - } - if testMeta.Filename != decryptedMsg.GetFilename() { - t.Fatalf("Expected filename to be %s got %s", testMeta.Filename, decryptedMsg.GetFilename()) - } - if testMeta.ModTime != int64(decryptedMsg.GetTime()) { - t.Fatalf("Expected modification time to be %d got %d", testMeta.ModTime, int64(decryptedMsg.GetTime())) - } -} - -func TestKeyRing_DecryptStreamCompatible(t *testing.T) { - messageBytes := []byte("Hello World!") - pgpMessage, err := keyRingTestPublic.Encrypt( - &PlainMessage{ - Data: messageBytes, - TextType: !testMeta.IsBinary, - Time: uint32(testMeta.ModTime), - Filename: testMeta.Filename, - }, - keyRingTestPrivate, - ) - if err != nil { - t.Fatal("Expected no error while encrypting plaintext, got:", err) - } - decryptedReader, err := keyRingTestPrivate.DecryptStream( - bytes.NewReader(pgpMessage.GetBinary()), - keyRingTestPublic, - GetUnixTime(), - ) - if err != nil { - t.Fatal("Expected no error while calling decrypting stream with key ring, got:", err) - } - decryptedBytes, err := ioutil.ReadAll(decryptedReader) - if err != nil { - t.Fatal("Expected no error while reading the decrypted data, got:", err) - } - err = decryptedReader.VerifySignature() - if err != nil { - t.Fatal("Expected no error while verifying the signature, got:", err) - } - if !bytes.Equal(decryptedBytes, messageBytes) { - t.Fatalf("Expected the decrypted data to be %s got %s", string(decryptedBytes), string(messageBytes)) - } - decryptedMeta := decryptedReader.GetMetadata() - if !reflect.DeepEqual(testMeta, decryptedMeta) { - t.Fatalf("Expected the decrypted metadata to be %v got %v", testMeta, decryptedMeta) - } -} - -func TestKeyRing_EncryptDecryptSplitStream(t *testing.T) { - messageBytes := []byte("Hello World!") - messageReader := bytes.NewReader(messageBytes) - var dataPacketBuf bytes.Buffer - encryptionResult, err := keyRingTestPublic.EncryptSplitStream( - &dataPacketBuf, - testMeta, - keyRingTestPrivate, - ) - if err != nil { - t.Fatal("Expected no error while calling encrypting split stream with key ring, got:", err) - } - messageWriter := encryptionResult - reachedEnd := false - bufferSize := 2 - buffer := make([]byte, bufferSize) - for !reachedEnd { - n, err := messageReader.Read(buffer) - if err != nil { - if errors.Is(err, io.EOF) { - reachedEnd = true - } else { - t.Fatal("Expected no error while reading data, got:", err) - } - } - writtenTotal := 0 - for writtenTotal < n { - written, err := messageWriter.Write(buffer[writtenTotal:n]) - if err != nil { - t.Fatal("Expected no error while writing data, got:", err) - } - writtenTotal += written - } - } - err = messageWriter.Close() - if err != nil { - t.Fatal("Expected no error while closing plaintext writer, got:", err) - } - keyPacket, err := encryptionResult.GetKeyPacket() - if err != nil { - t.Fatal("Expected no error while accessing key packet, got:", err) - } - dataPacket := dataPacketBuf.Bytes() - decryptedReader, err := keyRingTestPrivate.DecryptSplitStream( - keyPacket, - bytes.NewReader(dataPacket), - keyRingTestPublic, - GetUnixTime(), - ) - if err != nil { - t.Fatal("Expected no error while decrypting split stream with key ring, got:", err) - } - decryptedBytes, err := ioutil.ReadAll(decryptedReader) - if err != nil { - t.Fatal("Expected no error while reading the decrypted data, got:", err) - } - err = decryptedReader.VerifySignature() - if err != nil { - t.Fatal("Expected no error while verifying the signature, got:", err) - } - if !bytes.Equal(decryptedBytes, messageBytes) { - t.Fatalf("Expected the decrypted data to be %s got %s", string(decryptedBytes), string(messageBytes)) - } - decryptedMeta := decryptedReader.GetMetadata() - if !reflect.DeepEqual(testMeta, decryptedMeta) { - t.Fatalf("Expected the decrypted metadata to be %v got %v", testMeta, decryptedMeta) - } -} - -func TestKeyRing_EncryptDecryptSplitStreamWithCont(t *testing.T) { - messageBytes := []byte("Hello World!") - messageReader := bytes.NewReader(messageBytes) - - var dataPacketBuf bytes.Buffer - encryptionResult, err := keyRingTestPublic.EncryptSplitStreamWithContext( - &dataPacketBuf, - testMeta, - keyRingTestPrivate, - NewSigningContext(testContext, true), - ) - if err != nil { - t.Fatal("Expected no error while calling encrypting split stream with key ring, got:", err) - } - messageWriter := encryptionResult - _, err = io.Copy(messageWriter, messageReader) - if err != nil { - t.Fatal("Expected no error while copying plaintext writer, got:", err) - } - err = messageWriter.Close() - if err != nil { - t.Fatal("Expected no error while closing plaintext writer, got:", err) - } - keyPacket, err := encryptionResult.GetKeyPacket() - if err != nil { - t.Fatal("Expected no error while accessing key packet, got:", err) - } - dataPacket := dataPacketBuf.Bytes() - decryptedReader, err := keyRingTestPrivate.DecryptSplitStreamWithContext( - keyPacket, - bytes.NewReader(dataPacket), - keyRingTestPublic, - GetUnixTime(), - NewVerificationContext(testContext, true, 0), - ) - if err != nil { - t.Fatal("Expected no error while decrypting split stream with key ring, got:", err) - } - decryptedBytes, err := ioutil.ReadAll(decryptedReader) - if err != nil { - t.Fatal("Expected no error while reading the decrypted data, got:", err) - } - err = decryptedReader.VerifySignature() - if err != nil { - t.Fatal("Expected no error while verifying the signature, got:", err) - } - if !bytes.Equal(decryptedBytes, messageBytes) { - t.Fatalf("Expected the decrypted data to be %s got %s", string(decryptedBytes), string(messageBytes)) - } -} - -func TestKeyRing_EncryptSplitStreamCompatible(t *testing.T) { - enc := func(w io.Writer, meta *PlainMessageMetadata, kr *KeyRing) (*EncryptSplitResult, error) { - return keyRingTestPublic.EncryptSplitStream( - w, - meta, - kr, - ) - } - testKeyRing_EncryptSplitStreamCompatible(enc, t) -} - -func TestKeyRing_EncryptSplitStreamWithCompressionCompatible(t *testing.T) { - enc := func(w io.Writer, meta *PlainMessageMetadata, kr *KeyRing) (*EncryptSplitResult, error) { - return keyRingTestPublic.EncryptSplitStreamWithCompression( - w, - meta, - kr, - ) - } - testKeyRing_EncryptSplitStreamCompatible(enc, t) -} - -type keyringEncryptionSplitFunction = func(io.Writer, *PlainMessageMetadata, *KeyRing) (*EncryptSplitResult, error) - -func testKeyRing_EncryptSplitStreamCompatible(encrypt keyringEncryptionSplitFunction, t *testing.T) { - messageBytes := []byte("Hello World!") - messageReader := bytes.NewReader(messageBytes) - var dataPacketBuf bytes.Buffer - encryptionResult, err := encrypt( - &dataPacketBuf, - testMeta, - keyRingTestPrivate, - ) - if err != nil { - t.Fatal("Expected no error while calling encrypting split stream with key ring, got:", err) - } - messageWriter := encryptionResult - reachedEnd := false - bufferSize := 2 - buffer := make([]byte, bufferSize) - for !reachedEnd { - n, err := messageReader.Read(buffer) - if err != nil { - if errors.Is(err, io.EOF) { - reachedEnd = true - } else { - t.Fatal("Expected no error while reading data, got:", err) - } - } - writtenTotal := 0 - for writtenTotal < n { - written, err := messageWriter.Write(buffer[writtenTotal:n]) - if err != nil { - t.Fatal("Expected no error while writing data, got:", err) - } - writtenTotal += written - } - } - err = messageWriter.Close() - if err != nil { - t.Fatal("Expected no error while closing plaintext writer, got:", err) - } - keyPacket, err := encryptionResult.GetKeyPacket() - if err != nil { - t.Fatal("Expected no error while accessing key packet, got:", err) - } - dataPacket := dataPacketBuf.Bytes() - decryptedMsg, err := keyRingTestPrivate.Decrypt( - NewPGPSplitMessage(keyPacket, dataPacket).GetPGPMessage(), - keyRingTestPublic, - GetUnixTime(), - ) - if err != nil { - t.Fatal("Expected no error while decrypting split stream with key ring, got:", err) - } - decryptedBytes := decryptedMsg.GetBinary() - if err != nil { - t.Fatal("Expected no error while reading the decrypted data, got:", err) - } - if !bytes.Equal(decryptedBytes, messageBytes) { - t.Fatalf("Expected the decrypted data to be %s got %s", string(decryptedBytes), string(messageBytes)) - } - if testMeta.IsBinary != decryptedMsg.IsBinary() { - t.Fatalf("Expected isBinary to be %t got %t", testMeta.IsBinary, decryptedMsg.IsBinary()) - } - if testMeta.Filename != decryptedMsg.GetFilename() { - t.Fatalf("Expected filename to be %s got %s", testMeta.Filename, decryptedMsg.GetFilename()) - } - if testMeta.ModTime != int64(decryptedMsg.GetTime()) { - t.Fatalf("Expected modification time to be %d got %d", testMeta.ModTime, int64(decryptedMsg.GetTime())) - } -} - -func TestKeyRing_DecryptSplitStreamCompatible(t *testing.T) { - messageBytes := []byte("Hello World!") - pgpMessage, err := keyRingTestPublic.Encrypt( - &PlainMessage{ - Data: messageBytes, - TextType: !testMeta.IsBinary, - Time: uint32(testMeta.ModTime), - Filename: testMeta.Filename, - }, - keyRingTestPrivate, - ) - if err != nil { - t.Fatal("Expected no error while encrypting plaintext, got:", err) - } - armored, err := pgpMessage.GetArmored() - if err != nil { - t.Fatal("Expected no error while armoring ciphertext, got:", err) - } - splitMsg, err := NewPGPSplitMessageFromArmored(armored) - if err != nil { - t.Fatal("Expected no error while splitting the ciphertext, got:", err) - } - keyPacket := splitMsg.KeyPacket - if err != nil { - t.Fatal("Expected no error while accessing key packet, got:", err) - } - dataPacket := splitMsg.DataPacket - decryptedReader, err := keyRingTestPrivate.DecryptSplitStream( - keyPacket, - bytes.NewReader(dataPacket), - keyRingTestPublic, - GetUnixTime(), - ) - if err != nil { - t.Fatal("Expected no error while decrypting split stream with key ring, got:", err) - } - decryptedBytes, err := ioutil.ReadAll(decryptedReader) - if err != nil { - t.Fatal("Expected no error while reading the decrypted data, got:", err) - } - if !bytes.Equal(decryptedBytes, messageBytes) { - t.Fatalf("Expected the decrypted data to be %s got %s", string(decryptedBytes), string(messageBytes)) - } - err = decryptedReader.VerifySignature() - if err != nil { - t.Fatal("Expected no error while verifying the signature, got:", err) - } - decryptedMeta := decryptedReader.GetMetadata() - if !reflect.DeepEqual(testMeta, decryptedMeta) { - t.Fatalf("Expected the decrypted metadata to be %v got %v", testMeta, decryptedMeta) - } -} - -func TestKeyRing_SignVerifyDetachedStream(t *testing.T) { - messageBytes := []byte("Hello World!") - messageReader := bytes.NewReader(messageBytes) - signature, err := keyRingTestPrivate.SignDetachedStream(messageReader) - if err != nil { - t.Fatal("Expected no error while signing the message, got:", err) - } - _, err = messageReader.Seek(0, 0) - if err != nil { - t.Fatal("Expected no error while rewinding the message reader, got:", err) - } - err = keyRingTestPublic.VerifyDetachedStream(messageReader, signature, GetUnixTime()) - if err != nil { - t.Fatal("Expected no error while verifying the detached signature, got:", err) - } -} - -func TestKeyRing_SignVerifyDetachedStreamWithContext(t *testing.T) { - messageBytes := []byte("Hello World!") - messageReader := bytes.NewReader(messageBytes) - signature, err := keyRingTestPrivate.SignDetachedStreamWithContext(messageReader, NewSigningContext(testContext, true)) - if err != nil { - t.Fatal("Expected no error while signing the message, got:", err) - } - _, err = messageReader.Seek(0, 0) - if err != nil { - t.Fatal("Expected no error while rewinding the message reader, got:", err) - } - err = keyRingTestPublic.VerifyDetachedStreamWithContext(messageReader, signature, GetUnixTime(), NewVerificationContext(testContext, true, 0)) - if err != nil { - t.Fatal("Expected no error while verifying the detached signature, got:", err) - } -} - -func TestKeyRing_SignDetachedStreamCompatible(t *testing.T) { - messageBytes := []byte("Hello World!") - messageReader := bytes.NewReader(messageBytes) - signature, err := keyRingTestPrivate.SignDetachedStream(messageReader) - if err != nil { - t.Fatal("Expected no error while signing the message, got:", err) - } - err = keyRingTestPublic.VerifyDetached(NewPlainMessage(messageBytes), signature, GetUnixTime()) - if err != nil { - t.Fatal("Expected no error while verifying the detached signature, got:", err) - } -} - -func TestKeyRing_VerifyDetachedStreamCompatible(t *testing.T) { - messageBytes := []byte("Hello World!") - messageReader := bytes.NewReader(messageBytes) - signature, err := keyRingTestPrivate.SignDetached(NewPlainMessage(messageBytes)) - if err != nil { - t.Fatal("Expected no error while signing the message, got:", err) - } - _, err = messageReader.Seek(0, 0) - if err != nil { - t.Fatal("Expected no error while rewinding the message reader, got:", err) - } - err = keyRingTestPublic.VerifyDetachedStream(messageReader, signature, GetUnixTime()) - if err != nil { - t.Fatal("Expected no error while verifying the detached signature, got:", err) - } -} - -func TestKeyRing_SignVerifyDetachedEncryptedStream(t *testing.T) { - messageBytes := []byte("Hello World!") - messageReader := bytes.NewReader(messageBytes) - encSignature, err := keyRingTestPrivate.SignDetachedEncryptedStream(messageReader, keyRingTestPublic) - if err != nil { - t.Fatal("Expected no error while signing the message, got:", err) - } - _, err = messageReader.Seek(0, 0) - if err != nil { - t.Fatal("Expected no error while rewinding the message reader, got:", err) - } - err = keyRingTestPublic.VerifyDetachedEncryptedStream(messageReader, encSignature, keyRingTestPrivate, GetUnixTime()) - if err != nil { - t.Fatal("Expected no error while verifying the detached signature, got:", err) - } -} - -func TestKeyRing_SignDetachedEncryptedStreamCompatible(t *testing.T) { - messageBytes := []byte("Hello World!") - messageReader := bytes.NewReader(messageBytes) - encSignature, err := keyRingTestPrivate.SignDetachedEncryptedStream(messageReader, keyRingTestPublic) - if err != nil { - t.Fatal("Expected no error while signing the message, got:", err) - } - err = keyRingTestPublic.VerifyDetachedEncrypted(NewPlainMessage(messageBytes), encSignature, keyRingTestPrivate, GetUnixTime()) - if err != nil { - t.Fatal("Expected no error while verifying the detached signature, got:", err) - } -} - -func TestKeyRing_VerifyDetachedEncryptedStreamCompatible(t *testing.T) { - messageBytes := []byte("Hello World!") - messageReader := bytes.NewReader(messageBytes) - encSignature, err := keyRingTestPrivate.SignDetachedEncrypted(NewPlainMessage(messageBytes), keyRingTestPublic) - if err != nil { - t.Fatal("Expected no error while signing the message, got:", err) - } - _, err = messageReader.Seek(0, 0) - if err != nil { - t.Fatal("Expected no error while rewinding the message reader, got:", err) - } - err = keyRingTestPublic.VerifyDetachedEncryptedStream(messageReader, encSignature, keyRingTestPrivate, GetUnixTime()) - if err != nil { - t.Fatal("Expected no error while verifying the detached signature, got:", err) - } -} diff --git a/crypto/keyring_test.go b/crypto/keyring_test.go index ca4a2770..5cd5dcd3 100644 --- a/crypto/keyring_test.go +++ b/crypto/keyring_test.go @@ -2,15 +2,12 @@ package crypto import ( "crypto/rsa" - "errors" "testing" "github.com/stretchr/testify/assert" "github.com/ProtonMail/go-crypto/openpgp/ecdh" "github.com/ProtonMail/go-crypto/openpgp/eddsa" - - "github.com/ProtonMail/gopenpgp/v2/constants" ) var testSymmetricKey []byte @@ -126,7 +123,7 @@ func TestKeyIds(t *testing.T) { func TestMultipleKeyRing(t *testing.T) { assert.Exactly(t, 3, len(keyRingTestMultiple.entities)) assert.Exactly(t, 3, keyRingTestMultiple.CountEntities()) - assert.Exactly(t, 3, keyRingTestMultiple.CountDecryptionEntities()) + assert.Exactly(t, 3, keyRingTestMultiple.CountDecryptionEntities(testTime)) assert.Exactly(t, 3, len(keyRingTestMultiple.GetKeys())) @@ -145,7 +142,7 @@ func TestMultipleKeyRing(t *testing.T) { } assert.Exactly(t, 1, len(singleKeyRing.entities)) assert.Exactly(t, 1, singleKeyRing.CountEntities()) - assert.Exactly(t, 1, singleKeyRing.CountDecryptionEntities()) + assert.Exactly(t, 1, singleKeyRing.CountDecryptionEntities(testTime)) } func TestSerializeParse(t *testing.T) { @@ -215,77 +212,3 @@ func TestClearPrivateParams(t *testing.T) { assert.False(t, key.ClearPrivateParams()) } } - -func TestEncryptedDetachedSignature(t *testing.T) { - keyRingPrivate, err := keyRingTestPrivate.Copy() - if err != nil { - t.Fatal("Expected no error while copying keyring, got:", err) - } - keyRingPublic, err := keyRingTestPublic.Copy() - if err != nil { - t.Fatal("Expected no error while copying keyring, got:", err) - } - message := NewPlainMessageFromString("Hello World!") - encSign, err := keyRingPrivate.SignDetachedEncrypted(message, keyRingPublic) - if err != nil { - t.Fatal("Expected no error while encryptedSigning, got:", err) - } - err = keyRingPublic.VerifyDetachedEncrypted(message, encSign, keyRingPrivate, 0) - if err != nil { - t.Fatal("Expected no error while verifying encSignature, got:", err) - } - message2 := NewPlainMessageFromString("Bye!") - err = keyRingPublic.VerifyDetachedEncrypted(message2, encSign, keyRingPrivate, 0) - if err == nil { - t.Fatal("Expected an error while verifying bad encSignature, got nil") - } -} - -func TestKeyringCapabilities(t *testing.T) { - assert.True(t, keyRingTestPrivate.CanVerify()) - assert.True(t, keyRingTestPrivate.CanEncrypt()) - assert.True(t, keyRingTestPublic.CanVerify()) - assert.True(t, keyRingTestPublic.CanEncrypt()) - assert.True(t, keyRingTestMultiple.CanVerify()) - assert.True(t, keyRingTestMultiple.CanEncrypt()) -} - -func TestVerificationTime(t *testing.T) { - message := NewPlainMessageFromString("Hello") - pgp.latestServerTime = 1632312383 - defer func() { - pgp.latestServerTime = testTime - }() - enc, err := keyRingTestPublic.Encrypt( - message, - keyRingTestPrivate, - ) - - if err != nil { - t.Fatalf("Encryption error: %v", err) - } - _, err = keyRingTestPrivate.Decrypt( - enc, - keyRingTestPublic, - 392039755, - ) - if err == nil { - t.Fatal("No signature error") - } - castedErr := &SignatureVerificationError{} - isType := errors.As(err, castedErr) - if !isType { - t.Fatalf("No signature error %v", err) - } - if castedErr.Status != constants.SIGNATURE_FAILED { - t.Fatalf("Wrong status %v", castedErr) - } - _, err = keyRingTestPrivate.Decrypt( - enc, - keyRingTestPublic, - 0, - ) - if err != nil { - t.Fatalf("Got an error while decrypting %v", err) - } -} diff --git a/crypto/message.go b/crypto/message.go index 284098fe..29cd38b0 100644 --- a/crypto/message.go +++ b/crypto/message.go @@ -2,109 +2,85 @@ package crypto import ( "bytes" - "encoding/base64" "encoding/json" goerrors "errors" "io" - "io/ioutil" "regexp" - "strings" - "time" - "github.com/ProtonMail/go-crypto/openpgp/clearsign" "github.com/ProtonMail/go-crypto/openpgp/packet" - "github.com/ProtonMail/gopenpgp/v2/armor" - "github.com/ProtonMail/gopenpgp/v2/constants" - "github.com/ProtonMail/gopenpgp/v2/internal" + "github.com/ProtonMail/gopenpgp/v3/armor" + "github.com/ProtonMail/gopenpgp/v3/constants" + "github.com/ProtonMail/gopenpgp/v3/internal" "github.com/pkg/errors" ) // ---- MODELS ----- -// PlainMessage stores a plain text / unencrypted message. -type PlainMessage struct { - // The content of the message - Data []byte +type LiteralMetadata struct { // If the content is text or binary - TextType bool - // The file's latest modification time - Time uint32 + isUTF8 bool // The encrypted message's filename - Filename string + filename string + // The file's latest modification time + ModTime int64 } // PGPMessage stores a PGP-encrypted message. type PGPMessage struct { - // The content of the message - Data []byte -} - -// PGPSignature stores a PGP-encoded detached signature. -type PGPSignature struct { - // The content of the signature - Data []byte -} - -// PGPSplitMessage contains a separate session key packet and symmetrically -// encrypted data packet. -type PGPSplitMessage struct { + // KeyPacket references the PKESK and SKESK packets of the message + KeyPacket []byte + // DataPacket references the SEIPD or AEAD protected packet of the message DataPacket []byte - KeyPacket []byte + // DetachedSignature stores the encrypted detached signature. + // Nil when the signature is embedded in the data packet or not present. + DetachedSignature []byte + // detachedSignatureIsPlain indicates if the detached signature is not encrypted. + detachedSignatureIsPlain bool + // Signals that no armor checksum must be appended when armoring + omitArmorChecksum bool } -// A ClearTextMessage is a signed but not encrypted PGP message, -// i.e. the ones beginning with -----BEGIN PGP SIGNED MESSAGE-----. -type ClearTextMessage struct { - Data []byte - Signature []byte +type PGPMessageBuffer struct { + key *bytes.Buffer + data *bytes.Buffer + signature *bytes.Buffer } // ---- GENERATORS ----- -// NewPlainMessage generates a new binary PlainMessage ready for encryption, -// signature, or verification from the unencrypted binary data. -// This will encrypt the message with the binary flag and preserve the file as is. -func NewPlainMessage(data []byte) *PlainMessage { - return &PlainMessage{ - Data: clone(data), - TextType: false, - Filename: "", - Time: uint32(GetUnixTime()), - } -} - -// NewPlainMessageFromFile generates a new binary PlainMessage ready for encryption, -// signature, or verification from the unencrypted binary data. -// This will encrypt the message with the binary flag and preserve the file as is. -// It assigns a filename and a modification time. -func NewPlainMessageFromFile(data []byte, filename string, time uint32) *PlainMessage { - return &PlainMessage{ - Data: clone(data), - TextType: false, - Filename: filename, - Time: time, - } +// NewFileMetadata creates literal metadata. +func NewFileMetadata(isUTF8 bool, filename string, modTime int64) *LiteralMetadata { + return &LiteralMetadata{isUTF8: isUTF8, filename: filename, ModTime: modTime} } -// NewPlainMessageFromString generates a new text PlainMessage, -// ready for encryption, signature, or verification from an unencrypted string. -// This will encrypt the message with the text flag, canonicalize the line endings -// (i.e. set all of them to \r\n) and strip the trailing spaces for each line. -// This allows seamless conversion to clear text signed messages (see RFC 4880 5.2.1 and 7.1). -func NewPlainMessageFromString(text string) *PlainMessage { - return &PlainMessage{ - Data: []byte(internal.Canonicalize(text)), - TextType: true, - Filename: "", - Time: uint32(GetUnixTime()), - } +// NewMetadata creates new default literal metadata with utf-8 set to isUTF8. +func NewMetadata(isUTF8 bool) *LiteralMetadata { + return &LiteralMetadata{isUTF8: isUTF8} } // NewPGPMessage generates a new PGPMessage from the unarmored binary data. +// Clones the data for go-mobile compatibility. func NewPGPMessage(data []byte) *PGPMessage { - return &PGPMessage{ - Data: clone(data), + return NewPGPMessageWithCloneFlag(data, true) +} + +// NewPGPMessageWithCloneFlag generates a new PGPMessage from the unarmored binary data. +func NewPGPMessageWithCloneFlag(data []byte, doClone bool) *PGPMessage { + packetData := data + if doClone { + packetData = clone(data) + } + pgpMessage := &PGPMessage{ + DataPacket: packetData, + } + pgpMessage, err := pgpMessage.splitMessage() + if err != nil { + // If there is an error in split treat the data as data packets. + return &PGPMessage{ + DataPacket: packetData, + } } + return pgpMessage } // NewPGPMessageFromArmored generates a new PGPMessage from an armored string ready for decryption. @@ -114,149 +90,87 @@ func NewPGPMessageFromArmored(armored string) (*PGPMessage, error) { return nil, errors.Wrap(err, "gopenpgp: error in unarmoring message") } - message, err := ioutil.ReadAll(encryptedIO.Body) + message, err := io.ReadAll(encryptedIO.Body) if err != nil { return nil, errors.Wrap(err, "gopenpgp: error in reading armored message") } - - return &PGPMessage{ - Data: message, - }, nil -} - -// NewPGPSplitMessage generates a new PGPSplitMessage from the binary unarmored keypacket, -// datapacket, and encryption algorithm. -func NewPGPSplitMessage(keyPacket []byte, dataPacket []byte) *PGPSplitMessage { - return &PGPSplitMessage{ - KeyPacket: clone(keyPacket), - DataPacket: clone(dataPacket), - } -} - -// NewPGPSplitMessageFromArmored generates a new PGPSplitMessage by splitting an armored message into its -// session key packet and symmetrically encrypted data packet. -func NewPGPSplitMessageFromArmored(encrypted string) (*PGPSplitMessage, error) { - message, err := NewPGPMessageFromArmored(encrypted) - if err != nil { - return nil, err - } - - return message.SplitMessage() -} - -// NewPGPSignature generates a new PGPSignature from the unarmored binary data. -func NewPGPSignature(data []byte) *PGPSignature { - return &PGPSignature{ - Data: clone(data), - } -} - -// NewPGPSignatureFromArmored generates a new PGPSignature from the armored -// string ready for verification. -func NewPGPSignatureFromArmored(armored string) (*PGPSignature, error) { - encryptedIO, err := internal.Unarmor(armored) - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: error in unarmoring signature") + pgpMessage := &PGPMessage{ + DataPacket: message, } - - signature, err := ioutil.ReadAll(encryptedIO.Body) + pgpMessage, err = pgpMessage.splitMessage() if err != nil { - return nil, errors.Wrap(err, "gopenpgp: error in reading armored signature") + return nil, errors.Wrap(err, "gopenpgp: error in splitting message") } - - return &PGPSignature{ - Data: signature, - }, nil + return pgpMessage, nil } -// NewClearTextMessage generates a new ClearTextMessage from data and -// signature. -func NewClearTextMessage(data []byte, signature []byte) *ClearTextMessage { - return &ClearTextMessage{ - Data: clone(data), - Signature: clone(signature), +// NewPGPSplitMessage generates a new PGPSplitMessage from the binary unarmored keypacket and datapacket. +// Clones the slices for go-mobile compatibility. +func NewPGPSplitMessage(keyPacket []byte, dataPacket []byte) *PGPMessage { + return &PGPMessage{ + KeyPacket: clone(keyPacket), + DataPacket: clone(dataPacket), } } -// NewClearTextMessageFromArmored returns the message body and unarmored -// signature from a clearsigned message. -func NewClearTextMessageFromArmored(signedMessage string) (*ClearTextMessage, error) { - modulusBlock, rest := clearsign.Decode([]byte(signedMessage)) - if len(rest) != 0 { - return nil, errors.New("gopenpgp: extra data after modulus") - } - - signature, err := ioutil.ReadAll(modulusBlock.ArmoredSignature.Body) - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: error in reading cleartext message") +// NewPGPMessageBuffer creates a message buffer. +func NewPGPMessageBuffer() *PGPMessageBuffer { + return &PGPMessageBuffer{ + key: new(bytes.Buffer), + data: new(bytes.Buffer), + signature: new(bytes.Buffer), } - - return NewClearTextMessage(modulusBlock.Bytes, signature), nil } // ---- MODEL METHODS ----- -// GetBinary returns the binary content of the message as a []byte. -func (msg *PlainMessage) GetBinary() []byte { - return msg.Data -} - -// GetString returns the content of the message as a string. -func (msg *PlainMessage) GetString() string { - return sanitizeString(strings.ReplaceAll(string(msg.Data), "\r\n", "\n")) -} - -// GetBase64 returns the base-64 encoded binary content of the message as a -// string. -func (msg *PlainMessage) GetBase64() string { - return base64.StdEncoding.EncodeToString(msg.Data) -} - -// NewReader returns a New io.Reader for the binary data of the message. -func (msg *PlainMessage) NewReader() io.Reader { - return bytes.NewReader(msg.GetBinary()) -} - -// IsText returns whether the message is a text message. -func (msg *PlainMessage) IsText() bool { - return msg.TextType -} - -// IsBinary returns whether the message is a binary message. -func (msg *PlainMessage) IsBinary() bool { - return !msg.TextType -} - -// getFormattedTime returns the message (latest modification) Time as time.Time. -func (msg *PlainMessage) getFormattedTime() time.Time { - return time.Unix(int64(msg.Time), 0) -} - -// GetBinary returns the unarmored binary content of the message as a []byte. -func (msg *PGPMessage) GetBinary() []byte { - return msg.Data +// Bytes returns the unarmored binary content of the message as a []byte. +func (msg *PGPMessage) Bytes() []byte { + return append(msg.KeyPacket, msg.DataPacket...) } // NewReader returns a New io.Reader for the unarmored binary data of the // message. +// Not supported on go-mobile clients. func (msg *PGPMessage) NewReader() io.Reader { - return bytes.NewReader(msg.GetBinary()) + return bytes.NewReader(msg.Bytes()) +} + +// Armor returns the armored message as a string. +func (msg *PGPMessage) Armor() (string, error) { + if msg.KeyPacket == nil { + return "", errors.New("gopenpgp: missing key packets in pgp message") + } + if msg.omitArmorChecksum { + return armor.ArmorPGPMessageChecksum(msg.Bytes(), false) + } + return armor.ArmorPGPMessage(msg.Bytes()) } -// GetArmored returns the armored message as a string. -func (msg *PGPMessage) GetArmored() (string, error) { - return armor.ArmorWithType(msg.Data, constants.PGPMessageHeader) +// ArmorBytes returns the armored message as a string. +func (msg *PGPMessage) ArmorBytes() ([]byte, error) { + if msg.KeyPacket == nil { + return nil, errors.New("gopenpgp: missing key packets in pgp message") + } + if msg.omitArmorChecksum { + return armor.ArmorPGPMessageBytesChecksum(msg.Bytes(), false) + } + return armor.ArmorPGPMessageBytes(msg.Bytes()) } -// GetArmoredWithCustomHeaders returns the armored message as a string, with +// ArmorWithCustomHeaders returns the armored message as a string, with // the given headers. Empty parameters are omitted from the headers. -func (msg *PGPMessage) GetArmoredWithCustomHeaders(comment, version string) (string, error) { - return armor.ArmorWithTypeAndCustomHeaders(msg.Data, constants.PGPMessageHeader, version, comment) +func (msg *PGPMessage) ArmorWithCustomHeaders(comment, version string) (string, error) { + if msg.omitArmorChecksum { + return armor.ArmorWithTypeAndCustomHeadersChecksum(msg.Bytes(), constants.PGPMessageHeader, version, comment, false) + } + return armor.ArmorWithTypeAndCustomHeaders(msg.Bytes(), constants.PGPMessageHeader, version, comment) } -// GetEncryptionKeyIDs Returns the key IDs of the keys to which the session key is encrypted. -func (msg *PGPMessage) GetEncryptionKeyIDs() ([]uint64, bool) { - packets := packet.NewReader(bytes.NewReader(msg.Data)) +// EncryptionKeyIDs Returns the key IDs of the keys to which the session key is encrypted. +// Not supported on go-mobile clients use msg.HexEncryptionKeyIDsJson() instead. +func (msg *PGPMessage) EncryptionKeyIDs() ([]uint64, bool) { + packets := packet.NewReader(bytes.NewReader(msg.KeyPacket)) var err error var ids []uint64 var encryptedKey *packet.EncryptedKey @@ -283,16 +197,17 @@ Loop: return ids, false } -// GetHexEncryptionKeyIDs Returns the key IDs of the keys to which the session key is encrypted. -func (msg *PGPMessage) GetHexEncryptionKeyIDs() ([]string, bool) { - return getHexKeyIDs(msg.GetEncryptionKeyIDs()) +// HexEncryptionKeyIDs returns the key IDs of the keys to which the session key is encrypted. +// Not supported on go-mobile clients use msg.HexEncryptionKeyIDsJson() instead. +func (msg *PGPMessage) HexEncryptionKeyIDs() ([]string, bool) { + return hexKeyIDs(msg.EncryptionKeyIDs()) } -// GetHexEncryptionKeyIDsJson returns the key IDs of the keys to which the session key is encrypted as a JSON array. +// HexEncryptionKeyIDsJson returns the key IDs of the keys to which the session key is encrypted as a JSON array. // If an error occurs it returns nil. // Helper function for go-mobile clients. -func (msg *PGPMessage) GetHexEncryptionKeyIDsJson() []byte { - hexIds, ok := msg.GetHexEncryptionKeyIDs() +func (msg *PGPMessage) HexEncryptionKeyIDsJson() []byte { + hexIds, ok := msg.HexEncryptionKeyIDs() if !ok { return nil } @@ -303,20 +218,23 @@ func (msg *PGPMessage) GetHexEncryptionKeyIDsJson() []byte { return hexIdsJson } -// GetSignatureKeyIDs Returns the key IDs of the keys to which the (readable) signature packets are encrypted to. -func (msg *PGPMessage) GetSignatureKeyIDs() ([]uint64, bool) { - return getSignatureKeyIDs(msg.Data) +// SignatureKeyIDs returns the key IDs of the keys to which the (readable) signature packets are encrypted to. +// Not supported on go-mobile clients use msg.HexSignatureKeyIDsJson() instead. +func (msg *PGPMessage) SignatureKeyIDs() ([]uint64, bool) { + return SignatureKeyIDs(msg.DataPacket) } -// GetHexSignatureKeyIDs Returns the key IDs of the keys to which the session key is encrypted. -func (msg *PGPMessage) GetHexSignatureKeyIDs() ([]string, bool) { - return getHexKeyIDs(msg.GetSignatureKeyIDs()) +// HexSignatureKeyIDs returns the key IDs of the keys to which the session key is encrypted. +// Not supported on go-mobile clients use msg.HexSignatureKeyIDsJson() instead. +func (msg *PGPMessage) HexSignatureKeyIDs() ([]string, bool) { + return hexKeyIDs(msg.SignatureKeyIDs()) } -// GetHexSignatureKeyIDsJson returns the key IDs of the keys to which the (readable) signature packets -// are encrypted to as a JSON array. Helper function for go-mobile clients. -func (msg *PGPMessage) GetHexSignatureKeyIDsJson() []byte { - sigHexSigIds, ok := msg.GetHexSignatureKeyIDs() +// HexSignatureKeyIDsJson returns the key IDs of the keys to which the session key is encrypted as a JSON array. +// If an error occurs it returns nil. +// Helper function for go-mobile clients. +func (msg *PGPMessage) HexSignatureKeyIDsJson() []byte { + sigHexSigIds, ok := msg.HexSignatureKeyIDs() if !ok { return nil } @@ -327,35 +245,49 @@ func (msg *PGPMessage) GetHexSignatureKeyIDsJson() []byte { return sigHexKeyIdsJSON } -// GetBinaryDataPacket returns the unarmored binary datapacket as a []byte. -func (msg *PGPSplitMessage) GetBinaryDataPacket() []byte { +// BinaryDataPacket returns the unarmored binary datapacket as a []byte. +func (msg *PGPMessage) BinaryDataPacket() []byte { return msg.DataPacket } -// GetBinaryKeyPacket returns the unarmored binary keypacket as a []byte. -func (msg *PGPSplitMessage) GetBinaryKeyPacket() []byte { +// BinaryKeyPacket returns the unarmored binary keypacket as a []byte. +func (msg *PGPMessage) BinaryKeyPacket() []byte { return msg.KeyPacket } -// GetBinary returns the unarmored binary joined packets as a []byte. -func (msg *PGPSplitMessage) GetBinary() []byte { - return append(msg.KeyPacket, msg.DataPacket...) +// EncryptedDetachedSignature returns the encrypted detached signature of this message +// as a PGPMessage where the data is the encrypted signature. +// If no detached signature is present in this message, it returns nil. +func (msg *PGPMessage) EncryptedDetachedSignature() *PGPMessage { + if msg.DetachedSignature == nil || msg.detachedSignatureIsPlain { + return nil + } + return &PGPMessage{ + KeyPacket: msg.KeyPacket, + DataPacket: msg.DetachedSignature, + } } -// GetArmored returns the armored message as a string, with joined data and key -// packets. -func (msg *PGPSplitMessage) GetArmored() (string, error) { - return armor.ArmorWithType(msg.GetBinary(), constants.PGPMessageHeader) +// PlainDetachedSignature returns the plaintext detached signature of this message. +// If no plaintext detached signature is present in this message, it returns an error. +func (msg *PGPMessage) PlainDetachedSignature() ([]byte, error) { + if msg.DetachedSignature == nil || !msg.detachedSignatureIsPlain { + return nil, errors.New("gopenpgp: no plaintext detached signature found") + } + return msg.DetachedSignature, nil } -// GetPGPMessage joins asymmetric session key packet with the symmetric data -// packet to obtain a PGP message. -func (msg *PGPSplitMessage) GetPGPMessage() *PGPMessage { - return NewPGPMessage(append(msg.KeyPacket, msg.DataPacket...)) +// PlainDetachedSignatureArmor returns the armored plaintext detached signature of this message. +// If no plaintext detached signature is present or armoring fails it returns an error. +func (msg *PGPMessage) PlainDetachedSignatureArmor() ([]byte, error) { + if msg.DetachedSignature == nil || !msg.detachedSignatureIsPlain { + return nil, errors.New("gopenpgp: no plaintext detached signature found") + } + return armor.ArmorPGPSignatureBinary(msg.DetachedSignature) } // GetNumberOfKeyPackets returns the number of keys packets in this message. -func (msg *PGPSplitMessage) GetNumberOfKeyPackets() (int, error) { +func (msg *PGPMessage) GetNumberOfKeyPackets() (int, error) { bytesReader := bytes.NewReader(msg.KeyPacket) packets := packet.NewReader(bytesReader) var keyPacketCount int @@ -375,10 +307,10 @@ func (msg *PGPSplitMessage) GetNumberOfKeyPackets() (int, error) { return keyPacketCount, nil } -// SplitMessage splits the message into key and data packet(s). -// Parameters are for backwards compatibility and are unused. -func (msg *PGPMessage) SplitMessage() (*PGPSplitMessage, error) { - bytesReader := bytes.NewReader(msg.Data) +// splitMessage splits the message into key and data packet(s). +func (msg *PGPMessage) splitMessage() (*PGPMessage, error) { + data := msg.DataPacket + bytesReader := bytes.NewReader(data) packets := packet.NewReader(bytesReader) splitPoint := int64(0) Loop: @@ -397,68 +329,73 @@ Loop: break Loop } } - return &PGPSplitMessage{ - KeyPacket: clone(msg.Data[:splitPoint]), - DataPacket: clone(msg.Data[splitPoint:]), + return &PGPMessage{ + KeyPacket: data[:splitPoint], + DataPacket: data[splitPoint:], }, nil } -// SeparateKeyAndData splits the message into key and data packet(s). -// Parameters are for backwards compatibility and are unused. -// Deprecated: use SplitMessage(). -func (msg *PGPMessage) SeparateKeyAndData(_ int, _ int) (*PGPSplitMessage, error) { - return msg.SplitMessage() +// Filename returns the filename of the literal metadata. +func (msg *LiteralMetadata) Filename() string { + if msg == nil { + return "" + } + return msg.filename } -// GetBinary returns the unarmored binary content of the signature as a []byte. -func (sig *PGPSignature) GetBinary() []byte { - return sig.Data +// IsUtf8 returns whether the literal metadata is annotated with utf-8. +func (msg *LiteralMetadata) IsUtf8() bool { + if msg == nil { + return false + } + return msg.isUTF8 } -// GetArmored returns the armored signature as a string. -func (sig *PGPSignature) GetArmored() (string, error) { - return armor.ArmorWithType(sig.Data, constants.PGPSignatureHeader) +func (msg *LiteralMetadata) Time() int64 { + if msg == nil { + return 0 + } + return msg.ModTime } -// GetSignatureKeyIDs Returns the key IDs of the keys to which the (readable) signature packets are encrypted to. -func (sig *PGPSignature) GetSignatureKeyIDs() ([]uint64, bool) { - return getSignatureKeyIDs(sig.Data) -} +// PGPMessageBuffer implements the PGPSplitWriter interface -// GetHexSignatureKeyIDs Returns the key IDs of the keys to which the session key is encrypted. -func (sig *PGPSignature) GetHexSignatureKeyIDs() ([]string, bool) { - return getHexKeyIDs(sig.GetSignatureKeyIDs()) +func (mb *PGPMessageBuffer) Write(b []byte) (n int, err error) { + return mb.data.Write(b) } -// GetBinary returns the unarmored signed data as a []byte. -func (msg *ClearTextMessage) GetBinary() []byte { - return msg.Data +// PGPMessage returns the PGPMessage extracted from the internal buffers. +func (mb *PGPMessageBuffer) PGPMessage() *PGPMessage { + return mb.PGPMessageWithOptions(false, false) } -// GetString returns the unarmored signed data as a string. -func (msg *ClearTextMessage) GetString() string { - return string(msg.Data) +// PGPMessageWithOptions returns the PGPMessage extracted from the internal buffers. +// The isPlain flag indicates wether the detached signature is encrypted or plaintext, if any. +func (mb *PGPMessageBuffer) PGPMessageWithOptions(isPlain, omitArmorChecksum bool) *PGPMessage { + var detachedSignature []byte + if mb.signature.Len() > 0 { + detachedSignature = mb.signature.Bytes() + } + if mb.key.Len() == 0 { + pgpMessage := NewPGPMessage(mb.data.Bytes()) + pgpMessage.DetachedSignature = detachedSignature + return pgpMessage + } + return &PGPMessage{ + KeyPacket: mb.key.Bytes(), + DataPacket: mb.data.Bytes(), + DetachedSignature: detachedSignature, + detachedSignatureIsPlain: isPlain, + omitArmorChecksum: omitArmorChecksum, + } } -// GetBinarySignature returns the unarmored binary signature as a []byte. -func (msg *ClearTextMessage) GetBinarySignature() []byte { - return msg.Signature +func (mb *PGPMessageBuffer) Keys() Writer { + return mb.key } -// GetArmored armors plaintext and signature with the PGP SIGNED MESSAGE -// armoring. -func (msg *ClearTextMessage) GetArmored() (string, error) { - armSignature, err := armor.ArmorWithType(msg.GetBinarySignature(), constants.PGPSignatureHeader) - if err != nil { - return "", errors.Wrap(err, "gopenpgp: error in armoring cleartext message") - } - - str := "-----BEGIN PGP SIGNED MESSAGE-----\r\nHash: SHA512\r\n\r\n" - str += msg.GetString() - str += "\r\n" - str += armSignature - - return str, nil +func (mb *PGPMessageBuffer) Signature() Writer { + return mb.signature } // ---- UTILS ----- @@ -470,8 +407,10 @@ func IsPGPMessage(data string) bool { return re.MatchString(data) } -func getSignatureKeyIDs(data []byte) ([]uint64, bool) { - packets := packet.NewReader(bytes.NewReader(data)) +// SignatureKeyIDs returns the key identifiers of the keys that were used +// to create the signatures. +func SignatureKeyIDs(signature []byte) ([]uint64, bool) { + packets := packet.NewReader(bytes.NewReader(signature)) var err error var ids []uint64 var onePassSignaturePacket *packet.OnePassSignature @@ -505,7 +444,13 @@ Loop: return ids, false } -func getHexKeyIDs(keyIDs []uint64, ok bool) ([]string, bool) { +// SignatureHexKeyIDs returns the key identifiers of the keys that were used +// to create the signatures in hexadecimal form. +func SignatureHexKeyIDs(signature []byte) ([]string, bool) { + return hexKeyIDs(SignatureKeyIDs(signature)) +} + +func hexKeyIDs(keyIDs []uint64, ok bool) ([]string, bool) { hexIDs := make([]string, len(keyIDs)) for i, id := range keyIDs { @@ -514,3 +459,11 @@ func getHexKeyIDs(keyIDs []uint64, ok bool) ([]string, bool) { return hexIDs, ok } + +// clone returns a clone of the byte slice. Internal function used to make sure +// we don't retain a reference to external data. +func clone(input []byte) []byte { + data := make([]byte, len(input)) + copy(data, input) + return data +} diff --git a/crypto/message_getters.go b/crypto/message_getters.go deleted file mode 100644 index 7389dedb..00000000 --- a/crypto/message_getters.go +++ /dev/null @@ -1,14 +0,0 @@ -//go:build !android -// +build !android - -package crypto - -// GetFilename returns the file name of the message as a string. -func (msg *PlainMessage) GetFilename() string { - return msg.Filename -} - -// GetTime returns the modification time of a file (if provided in the ciphertext). -func (msg *PlainMessage) GetTime() uint32 { - return msg.Time -} diff --git a/crypto/message_test.go b/crypto/message_test.go index 906955a4..40d2da85 100644 --- a/crypto/message_test.go +++ b/crypto/message_test.go @@ -3,25 +3,26 @@ package crypto import ( "bytes" "encoding/base64" - "errors" "io" - "io/ioutil" + "os" "testing" "time" "github.com/ProtonMail/go-crypto/openpgp/packet" + "github.com/pkg/errors" "github.com/stretchr/testify/assert" ) func TestTextMessageEncryptionWithPassword(t *testing.T) { - var message = NewPlainMessageFromString("The secret code is... 1, 2, 3, 4, 5") + var message = []byte("The secret code is... 1, 2, 3, 4, 5") // Encrypt data with password - encrypted, err := EncryptMessageWithPassword(message, testSymmetricKey) + encryptor, _ := testPGP.Encryption().Password(testSymmetricKey).New() + encrypted, err := encryptor.Encrypt(message) if err != nil { t.Fatal("Expected no error when encrypting, got:", err) } - packets := packet.NewReader(bytes.NewReader(encrypted.GetBinary())) + packets := packet.NewReader(bytes.NewReader(encrypted.Bytes())) var foundSk bool for { var p packet.Packet @@ -40,36 +41,41 @@ func TestTextMessageEncryptionWithPassword(t *testing.T) { t.Fatal("Expect to found encrypted session key") } // Decrypt data with wrong password - _, err = DecryptMessageWithPassword(encrypted, []byte("Wrong password")) + decryptorWrong, _ := testPGP.Decryption().Password([]byte("Wrong password")).New() + _, err = decryptorWrong.Decrypt(encrypted.Bytes(), Bytes) assert.NotNil(t, err) // Decrypt data with the good password - decrypted, err := DecryptMessageWithPassword(encrypted, testSymmetricKey) + decryptor, _ := testPGP.Decryption().Password(testSymmetricKey).New() + decrypted, err := decryptor.Decrypt(encrypted.Bytes(), Bytes) if err != nil { t.Fatal("Expected no error when decrypting, got:", err) } - assert.Exactly(t, message.GetString(), decrypted.GetString()) + assert.Exactly(t, string(message), string(decrypted.Bytes())) } func TestBinaryMessageEncryptionWithPassword(t *testing.T) { binData, _ := base64.StdEncoding.DecodeString("ExXmnSiQ2QCey20YLH6qlLhkY3xnIBC1AwlIXwK/HvY=") - var message = NewPlainMessage(binData) + var message = binData // Encrypt data with password - encrypted, err := EncryptMessageWithPassword(message, testSymmetricKey) + encryptor, _ := testPGP.Encryption().Password(testSymmetricKey).New() + encrypted, err := encryptor.Encrypt(message) if err != nil { t.Fatal("Expected no error when encrypting, got:", err) } // Decrypt data with wrong password - _, err = DecryptMessageWithPassword(encrypted, []byte("Wrong password")) + decryptorWrong, _ := testPGP.Decryption().Password([]byte("Wrong password")).New() + _, err = decryptorWrong.Decrypt(encrypted.Bytes(), Bytes) assert.NotNil(t, err) // Decrypt data with the good password - decrypted, err := DecryptMessageWithPassword(encrypted, testSymmetricKey) + decryptor, _ := testPGP.Decryption().Password(testSymmetricKey).New() + decrypted, err := decryptor.Decrypt(encrypted.Bytes(), Bytes) if err != nil { t.Fatal("Expected no error when decrypting, got:", err) } - assert.Exactly(t, message, decrypted) + assert.Exactly(t, message, decrypted.Bytes()) } func TestTextMixedMessageDecryptionWithPassword(t *testing.T) { @@ -79,143 +85,77 @@ func TestTextMixedMessageDecryptionWithPassword(t *testing.T) { } // Decrypt data with the good password - decrypted, err := DecryptMessageWithPassword(encrypted, []byte("pinata")) + decryptor, _ := testPGP.Decryption().Password([]byte("pinata")).New() + decrypted, err := decryptor.Decrypt(encrypted.Bytes(), Bytes) if err != nil { t.Fatal("Expected no error when decrypting, got:", err) } - expected, err := ioutil.ReadFile("testdata/message_mixedPasswordPublicExpected") + expected, err := os.ReadFile("testdata/message_mixedPasswordPublicExpected") if err != nil { panic(err) } - assert.Exactly(t, expected, decrypted.GetBinary()) + assert.Exactly(t, expected, decrypted.Bytes()) } func TestTextMessageEncryption(t *testing.T) { - var message = NewPlainMessageFromString( + var message = []byte( "The secret code is... 1, 2, 3, 4, 5. I repeat: the secret code is... 1, 2, 3, 4, 5", ) - - ciphertext, err := keyRingTestPublic.Encrypt(message, nil) + encryptor, _ := testPGP.Encryption().Recipients(keyRingTestPublic).New() + ciphertext, err := encryptor.Encrypt(message) if err != nil { t.Fatal("Expected no error when encrypting, got:", err) } - split, err := ciphertext.SplitMessage() - if err != nil { - t.Fatal("Expected no error when splitting, got:", err) - } - - assert.Len(t, split.GetBinaryDataPacket(), 133) // Assert uncompressed encrypted body length + assert.Len(t, ciphertext.BinaryDataPacket(), 133) // Assert uncompressed encrypted body length - decrypted, err := keyRingTestPrivate.Decrypt(ciphertext, nil, 0) + decryptor, _ := testPGP.Decryption().DecryptionKeys(keyRingTestPrivate).New() + decrypted, err := decryptor.Decrypt(ciphertext.Bytes(), Bytes) if err != nil { t.Fatal("Expected no error when decrypting, got:", err) } - assert.Exactly(t, message.GetString(), decrypted.GetString()) + assert.Exactly(t, message, decrypted.Bytes()) } func TestTextMessageEncryptionWithTrailingSpaces(t *testing.T) { var original = "The secret code is... 1, 2, 3, 4, 5. I repeat: the secret code is... 1, 2, 3, 4, 5 " - var message = NewPlainMessageFromString(original) + var message = []byte(original) - ciphertext, err := keyRingTestPublic.Encrypt(message, nil) + encryptor, _ := testPGP.Encryption().Recipients(keyRingTestPublic).New() + ciphertext, err := encryptor.Encrypt(message) if err != nil { t.Fatal("Expected no error when encrypting, got:", err) } - decrypted, err := keyRingTestPrivate.Decrypt(ciphertext, nil, 0) + decryptor, _ := testPGP.Decryption().DecryptionKeys(keyRingTestPrivate).New() + decrypted, err := decryptor.Decrypt(ciphertext.Bytes(), Bytes) if err != nil { t.Fatal("Expected no error when decrypting, got:", err) } - assert.Exactly(t, original, decrypted.GetString()) + assert.Exactly(t, message, decrypted.Bytes()) } func TestTextMessageEncryptionWithNonCanonicalLinebreak(t *testing.T) { var original = "The secret code is... 1, 2, 3, 4, 5. I repeat: the secret code is... 1, 2, 3, 4, 5 \n \n" - var message = NewPlainMessageFromString(original) + var message = []byte(original) - ciphertext, err := keyRingTestPublic.Encrypt(message, nil) + encryptor, _ := testPGP.Encryption().Recipients(keyRingTestPublic).New() + ciphertext, err := encryptor.Encrypt(message) if err != nil { t.Fatal("Expected no error when encrypting, got:", err) } - decrypted, err := keyRingTestPrivate.Decrypt(ciphertext, nil, 0) + decryptor, _ := testPGP.Decryption().DecryptionKeys(keyRingTestPrivate).New() + decrypted, err := decryptor.Decrypt(ciphertext.Bytes(), Bytes) if err != nil { t.Fatal("Expected no error when decrypting, got:", err) } - assert.Exactly(t, original, decrypted.GetString()) -} - -func TestTextMessageEncryptionWithCompression(t *testing.T) { - var message = NewPlainMessageFromString( - "The secret code is... 1, 2, 3, 4, 5. I repeat: the secret code is... 1, 2, 3, 4, 5", - ) - - ciphertext, err := keyRingTestPublic.EncryptWithCompression(message, nil) - if err != nil { - t.Fatal("Expected no error when encrypting, got:", err) - } - - split, err := ciphertext.SplitMessage() - if err != nil { - t.Fatal("Expected no error when splitting, got:", err) - } - - assert.Len(t, split.GetBinaryDataPacket(), 117) // Assert uncompressed encrypted body length - - decrypted, err := keyRingTestPrivate.Decrypt(ciphertext, nil, 0) - if err != nil { - t.Fatal("Expected no error when decrypting, got:", err) - } - assert.Exactly(t, message.GetString(), decrypted.GetString()) -} - -func TestTextMessageEncryptionWithSignature(t *testing.T) { - var message = NewPlainMessageFromString("plain text") - - ciphertext, err := keyRingTestPublic.Encrypt(message, keyRingTestPrivate) - if err != nil { - t.Fatal("Expected no error when encrypting, got:", err) - } - - decrypted, err := keyRingTestPrivate.Decrypt(ciphertext, keyRingTestPublic, GetUnixTime()) - if err != nil { - t.Fatal("Expected no error when decrypting, got:", err) - } - assert.Exactly(t, message.GetString(), decrypted.GetString()) -} - -func TestBinaryMessageEncryption(t *testing.T) { - binData, _ := base64.StdEncoding.DecodeString("ExXmnSiQ2QCey20YLH6qlLhkY3xnIBC1AwlIXwK/HvY=") - var message = NewPlainMessage(binData) - - ciphertext, err := keyRingTestPublic.Encrypt(message, keyRingTestPrivate) - if err != nil { - t.Fatal("Expected no error when encrypting, got:", err) - } - - decrypted, err := keyRingTestPrivate.Decrypt(ciphertext, keyRingTestPublic, GetUnixTime()) - if err != nil { - t.Fatal("Expected no error when decrypting, got:", err) - } - assert.Exactly(t, message.GetBinary(), decrypted.GetBinary()) - - // Decrypt without verifying - decrypted, err = keyRingTestPrivate.Decrypt(ciphertext, nil, 0) - if err != nil { - t.Fatal("Expected no error when decrypting, got:", err) - } - assert.Exactly(t, message.GetString(), decrypted.GetString()) + assert.Exactly(t, message, decrypted.Bytes()) } func TestIssue11(t *testing.T) { - pgp.latestServerTime = 1559655272 - defer func() { - pgp.latestServerTime = testTime - }() - var issue11Password = []byte("1234") issue11Key, err := NewKeyFromArmored(readTestFile("issue11_privatekey", false)) @@ -248,19 +188,22 @@ func TestIssue11(t *testing.T) { if err != nil { t.Fatal("Expected no error while reading ciphertext, got:", err) } - - plainMessage, err := issue11Keyring.Decrypt(pgpMessage, senderKeyring, 0) + decryptor, _ := testPGP.Decryption(). + DecryptionKeys(issue11Keyring). + VerificationKeys(senderKeyring). + VerifyTime(1559655883).New() + decrypted, err := decryptor.Decrypt(pgpMessage.Bytes(), Bytes) if err != nil { t.Fatal("Expected no error while decrypting/verifying, got:", err) } + if err = decrypted.SignatureError(); err != nil { + t.Fatal("Expected no error while decrypting/verifying, got:", err) + } - assert.Exactly(t, "message from sender", plainMessage.GetString()) + assert.Exactly(t, "message from sender", string(decrypted.Bytes())) } func TestDummy(t *testing.T) { - pgp.latestServerTime = 1636644417 - defer func() { pgp.latestServerTime = testTime }() - dummyKey, err := NewKeyFromArmored(readTestFile("key_dummy", false)) if err != nil { t.Fatal("Expected no error while unarmoring public keyring, got:", err) @@ -271,7 +214,7 @@ func TestDummy(t *testing.T) { t.Fatal("Expected no error while unlocking private key, got:", err) } - _, err = unlockedDummyKey.Lock([]byte("golang")) + _, err = testPGP.LockKey(unlockedDummyKey, []byte("golang")) if err != nil { t.Fatal("Expected no error while unlocking private key, got:", err) } @@ -281,27 +224,24 @@ func TestDummy(t *testing.T) { t.Fatal("Expected no error while building private keyring, got:", err) } - var message = NewPlainMessageFromString( + var message = []byte( "The secret code is... 1, 2, 3, 4, 5. I repeat: the secret code is... 1, 2, 3, 4, 5", ) - ciphertext, err := dummyKeyRing.Encrypt(message, nil) + encryptor, _ := testPGP.Encryption().SignTime(1636644417).Recipients(dummyKeyRing).New() + ciphertext, err := encryptor.Encrypt(message) if err != nil { t.Fatal("Expected no error when encrypting, got:", err) } - split, err := ciphertext.SplitMessage() - if err != nil { - t.Fatal("Expected no error when splitting, got:", err) - } + assert.Len(t, ciphertext.BinaryDataPacket(), 133) // Assert uncompressed encrypted body length - assert.Len(t, split.GetBinaryDataPacket(), 133) // Assert uncompressed encrypted body length - - decrypted, err := dummyKeyRing.Decrypt(ciphertext, nil, 0) + decryptor, _ := testPGP.Decryption().DecryptionKeys(dummyKeyRing).New() + decrypted, err := decryptor.Decrypt(ciphertext.Bytes(), Bytes) if err != nil { t.Fatal("Expected no error when decrypting, got:", err) } - assert.Exactly(t, message.GetString(), decrypted.GetString()) + assert.Exactly(t, message, decrypted.Bytes()) } func TestSignedMessageDecryption(t *testing.T) { @@ -310,11 +250,12 @@ func TestSignedMessageDecryption(t *testing.T) { t.Fatal("Expected no error when unarmoring, got:", err) } - decrypted, err := keyRingTestPrivate.Decrypt(pgpMessage, nil, 0) + decryptor, _ := testPGP.Decryption().DecryptionKeys(keyRingTestPrivate).New() + decrypted, err := decryptor.Decrypt(pgpMessage.Bytes(), Bytes) if err != nil { t.Fatal("Expected no error when decrypting, got:", err) } - assert.Exactly(t, readTestFile("message_plaintext", true), decrypted.GetString()) + assert.Exactly(t, readTestFile("message_plaintext", true), string(decrypted.Bytes())) } func TestSHA256SignedMessageDecryption(t *testing.T) { @@ -323,11 +264,19 @@ func TestSHA256SignedMessageDecryption(t *testing.T) { t.Fatal("Expected no error when unarmoring, got:", err) } - decrypted, err := keyRingTestPrivate.Decrypt(pgpMessage, keyRingTestPrivate, 0) + decryptor, _ := testPGP.Decryption(). + DecryptionKeys(keyRingTestPrivate). + VerificationKeys(keyRingTestPrivate). + DisableVerifyTimeCheck(). + New() + decrypted, err := decryptor.Decrypt(pgpMessage.Bytes(), Bytes) if err != nil { t.Fatal("Expected no error when decrypting, got:", err) } - assert.Exactly(t, readTestFile("message_plaintext", true), decrypted.GetString()) + if err = decrypted.SignatureError(); err != nil { + t.Fatal("Expected no signature error when decrypting, got:", err) + } + assert.Exactly(t, readTestFile("message_plaintext", true), string(decrypted.Bytes())) } func TestSHA1SignedMessageDecryption(t *testing.T) { @@ -336,21 +285,30 @@ func TestSHA1SignedMessageDecryption(t *testing.T) { t.Fatal("Expected no error when unarmoring, got:", err) } - decrypted, err := keyRingTestPrivate.Decrypt(pgpMessage, keyRingTestPrivate, 0) - if err == nil { + decryptor, _ := testPGP.Decryption(). + DecryptionKeys(keyRingTestPrivate). + VerificationKeys(keyRingTestPrivate). + DisableVerifyTimeCheck(). + New() + decrypted, err := decryptor.Decrypt(pgpMessage.Bytes(), Bytes) + if err != nil { + t.Fatal("Expected no error, got: ", err) + } + if err = decrypted.SignatureError(); err == nil { t.Fatal("Expected verification error when decrypting") } - if err.Error() != "Signature Verification Error: Insecure signature" { - t.Fatal("Expected verification error when decrypting, got:", err) + if errStr := decrypted.SignatureError().Error(); errStr != "Signature Verification Error: Invalid signature caused by openpgp: invalid signature: insecure message hash algorithm: SHA-1" { + t.Fatal("Expected verification error when decrypting, got:", errStr) } - assert.Exactly(t, readTestFile("message_plaintext", true), decrypted.GetString()) + assert.Exactly(t, readTestFile("message_plaintext", true), string(decrypted.Bytes())) } func TestMultipleKeyMessageEncryption(t *testing.T) { - var message = NewPlainMessageFromString("plain text") + var message = []byte("plain text") assert.Exactly(t, 3, len(keyRingTestMultiple.entities)) - ciphertext, err := keyRingTestMultiple.Encrypt(message, keyRingTestPrivate) + encryptor, _ := testPGP.Encryption().Recipients(keyRingTestMultiple).SigningKeys(keyRingTestPrivate).New() + ciphertext, err := encryptor.Encrypt(message) if err != nil { t.Fatal("Expected no error when encrypting, got:", err) } @@ -358,7 +316,7 @@ func TestMultipleKeyMessageEncryption(t *testing.T) { // Test that ciphertext data contains three Encrypted Key Packets (tag 1) // followed by a single symmetrically encrypted data packet (tag 18) var p packet.Packet - packets := packet.NewReader(bytes.NewReader(ciphertext.Data)) + packets := packet.NewReader(bytes.NewReader(ciphertext.Bytes())) for i := 0; i < 3; i++ { if p, err = packets.Next(); err != nil { t.Fatal(err.Error()) @@ -375,25 +333,33 @@ func TestMultipleKeyMessageEncryption(t *testing.T) { } // Decrypt message and verify correctness - decrypted, err := keyRingTestPrivate.Decrypt(ciphertext, keyRingTestPublic, GetUnixTime()) + decryptor, _ := testPGP.Decryption(). + DecryptionKeys(keyRingTestPrivate). + VerificationKeys(keyRingTestPublic). + New() + decrypted, err := decryptor.Decrypt(ciphertext.Bytes(), Bytes) if err != nil { t.Fatal("Expected no error when decrypting, got:", err) } - assert.Exactly(t, message.GetString(), decrypted.GetString()) + if err = decrypted.SignatureError(); err != nil { + t.Fatal("Expected no signature error when decrypting, got:", err) + } + assert.Exactly(t, message, decrypted.Bytes()) } func TestMessageGetEncryptionKeyIDs(t *testing.T) { - var message = NewPlainMessageFromString("plain text") + var message = []byte("plain text") assert.Exactly(t, 3, len(keyRingTestMultiple.entities)) - ciphertext, err := keyRingTestMultiple.Encrypt(message, keyRingTestPrivate) + encryptor, _ := testPGP.Encryption().Recipients(keyRingTestMultiple).SigningKeys(keyRingTestPrivate).New() + ciphertext, err := encryptor.Encrypt(message) if err != nil { t.Fatal("Expected no error when encrypting, got:", err) } - ids, ok := ciphertext.GetEncryptionKeyIDs() + ids, ok := ciphertext.EncryptionKeyIDs() assert.Exactly(t, 3, len(ids)) assert.True(t, ok) - encKey, ok := keyRingTestMultiple.entities[0].EncryptionKey(time.Now()) + encKey, ok := keyRingTestMultiple.entities[0].EncryptionKey(time.Now(), nil) assert.True(t, ok) assert.Exactly(t, encKey.PublicKey.KeyId, ids[0]) } @@ -404,7 +370,7 @@ func TestMessageGetHexGetEncryptionKeyIDs(t *testing.T) { t.Fatal("Expected no error when reading message, got:", err) } - ids, ok := ciphertext.GetHexEncryptionKeyIDs() + ids, ok := ciphertext.HexEncryptionKeyIDs() assert.Exactly(t, 2, len(ids)) assert.True(t, ok) @@ -413,17 +379,18 @@ func TestMessageGetHexGetEncryptionKeyIDs(t *testing.T) { } func TestMessageGetSignatureKeyIDs(t *testing.T) { - var message = NewPlainMessageFromString("plain text") + var message = []byte("plain text") - signature, err := keyRingTestPrivate.SignDetached(message) + signer, _ := testPGP.Sign().SigningKeys(keyRingTestPrivate).Detached().New() + signature, err := signer.Sign(message, Bytes) if err != nil { t.Fatal("Expected no error when encrypting, got:", err) } - ids, ok := signature.GetSignatureKeyIDs() + ids, ok := SignatureKeyIDs(signature) assert.Exactly(t, 1, len(ids)) assert.True(t, ok) - signingKey, ok := keyRingTestPrivate.entities[0].SigningKey(time.Now()) + signingKey, ok := keyRingTestPrivate.entities[0].SigningKey(time.Now(), nil) assert.True(t, ok) assert.Exactly(t, signingKey.PublicKey.KeyId, ids[0]) } @@ -434,7 +401,7 @@ func TestMessageGetHexSignatureKeyIDs(t *testing.T) { t.Fatal("Expected no error when reading message, got:", err) } - ids, ok := ciphertext.GetHexSignatureKeyIDs() + ids, ok := ciphertext.HexSignatureKeyIDs() assert.Exactly(t, 2, len(ids)) assert.True(t, ok) @@ -443,15 +410,16 @@ func TestMessageGetHexSignatureKeyIDs(t *testing.T) { } func TestMessageGetArmoredWithCustomHeaders(t *testing.T) { - var message = NewPlainMessageFromString("plain text") + var message = []byte("plain text") - ciphertext, err := keyRingTestPublic.Encrypt(message, keyRingTestPrivate) + encryptor, _ := testPGP.Encryption().Recipients(keyRingTestPublic).SigningKeys(keyRingTestPrivate).New() + ciphertext, err := encryptor.Encrypt(message) if err != nil { t.Fatal("Expected no error when encrypting, got:", err) } comment := "User-defined comment" version := "User-defined version" - armored, err := ciphertext.GetArmoredWithCustomHeaders(comment, version) + armored, err := ciphertext.ArmorWithCustomHeaders(comment, version) if err != nil { t.Fatal("Could not armor the ciphertext:", err) } @@ -461,15 +429,16 @@ func TestMessageGetArmoredWithCustomHeaders(t *testing.T) { } func TestMessageGetArmoredWithEmptyHeaders(t *testing.T) { - var message = NewPlainMessageFromString("plain text") + var message = []byte("plain text") - ciphertext, err := keyRingTestPublic.Encrypt(message, keyRingTestPrivate) + encryptor, _ := testPGP.Encryption().Recipients(keyRingTestPublic).SigningKeys(keyRingTestPrivate).New() + ciphertext, err := encryptor.Encrypt(message) if err != nil { t.Fatal("Expected no error when encrypting, got:", err) } comment := "" version := "" - armored, err := ciphertext.GetArmoredWithCustomHeaders(comment, version) + armored, err := ciphertext.ArmorWithCustomHeaders(comment, version) if err != nil { t.Fatal("Could not armor the ciphertext:", err) } @@ -489,14 +458,14 @@ vA== =YNf2 -----END PGP MESSAGE----- ` - split, err := NewPGPSplitMessageFromArmored(message) + msg, err := NewPGPMessageFromArmored(message) if err != nil { t.Errorf("Couldn't parse split message: %v", err) } - if split.KeyPacket == nil { + if msg.KeyPacket == nil { t.Error("Key packet was nil") } - if split.DataPacket == nil { + if msg.DataPacket == nil { t.Error("Data packet was nil") } } diff --git a/crypto/password.go b/crypto/password.go deleted file mode 100644 index 40ebad9e..00000000 --- a/crypto/password.go +++ /dev/null @@ -1,180 +0,0 @@ -package crypto - -import ( - "bytes" - "io" - - "github.com/ProtonMail/go-crypto/openpgp" - pgpErrors "github.com/ProtonMail/go-crypto/openpgp/errors" - "github.com/ProtonMail/go-crypto/openpgp/packet" - "github.com/pkg/errors" -) - -// EncryptMessageWithPassword encrypts a PlainMessage to PGPMessage with a -// SymmetricKey. -// * message : The plain data as a PlainMessage. -// * password: A password that will be derived into an encryption key. -// * output : The encrypted data as PGPMessage. -func EncryptMessageWithPassword(message *PlainMessage, password []byte) (*PGPMessage, error) { - encrypted, err := passwordEncrypt(message, password) - if err != nil { - return nil, err - } - - return NewPGPMessage(encrypted), nil -} - -// DecryptMessageWithPassword decrypts password protected pgp binary messages. -// * encrypted: The encrypted data as PGPMessage. -// * password: A password that will be derived into an encryption key. -// * output: The decrypted data as PlainMessage. -func DecryptMessageWithPassword(message *PGPMessage, password []byte) (*PlainMessage, error) { - return passwordDecrypt(message.NewReader(), password) -} - -// DecryptSessionKeyWithPassword decrypts the binary symmetrically encrypted -// session key packet and returns the session key. -func DecryptSessionKeyWithPassword(keyPacket, password []byte) (*SessionKey, error) { - keyReader := bytes.NewReader(keyPacket) - packets := packet.NewReader(keyReader) - - var symKeys []*packet.SymmetricKeyEncrypted - for { - var p packet.Packet - var err error - if p, err = packets.Next(); err != nil { - break - } - - if p, ok := p.(*packet.SymmetricKeyEncrypted); ok { - symKeys = append(symKeys, p) - } - } - - // Try the symmetric passphrase first - if len(symKeys) != 0 && password != nil { - for _, s := range symKeys { - key, cipherFunc, err := s.Decrypt(password) - if err == nil { - sk := &SessionKey{ - V6: s.Version == 6, - Key: key, - Algo: getAlgo(cipherFunc), - } - - if err = sk.checkSize(); err != nil { - return nil, errors.Wrap(err, "gopenpgp: unable to decrypt session key with password") - } - - return sk, nil - } - } - } - - return nil, errors.New("gopenpgp: unable to decrypt any packet") -} - -// EncryptSessionKeyWithPassword encrypts the session key with the password and -// returns a binary symmetrically encrypted session key packet. -func EncryptSessionKeyWithPassword(sk *SessionKey, password []byte) ([]byte, error) { - outbuf := &bytes.Buffer{} - - cf, err := sk.GetCipherFunc() - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: unable to encrypt session key with password") - } - - if len(password) == 0 { - return nil, errors.New("gopenpgp: password can't be empty") - } - - if err = sk.checkSize(); err != nil { - return nil, errors.Wrap(err, "gopenpgp: unable to encrypt session key with password") - } - - config := &packet.Config{ - DefaultCipher: cf, - } - - err = packet.SerializeSymmetricKeyEncryptedReuseKey(outbuf, sk.Key, password, config) - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: unable to encrypt session key with password") - } - return outbuf.Bytes(), nil -} - -// ----- INTERNAL FUNCTIONS ------ - -func passwordEncrypt(message *PlainMessage, password []byte) ([]byte, error) { - var outBuf bytes.Buffer - - config := &packet.Config{ - DefaultCipher: packet.CipherAES256, - Time: getTimeGenerator(), - } - - hints := &openpgp.FileHints{ - IsBinary: message.IsBinary(), - FileName: message.Filename, - ModTime: message.getFormattedTime(), - } - - encryptWriter, err := openpgp.SymmetricallyEncrypt(&outBuf, password, hints, config) - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: error in encrypting message symmetrically") - } - _, err = encryptWriter.Write(message.GetBinary()) - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: error in writing data to message") - } - - err = encryptWriter.Close() - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: error in closing writer") - } - - return outBuf.Bytes(), nil -} - -func passwordDecrypt(encryptedIO io.Reader, password []byte) (*PlainMessage, error) { - firstTimeCalled := true - var prompt = func(keys []openpgp.Key, symmetric bool) ([]byte, error) { - if firstTimeCalled { - firstTimeCalled = false - return password, nil - } - // Re-prompt still occurs if SKESK pasrsing fails (i.e. when decrypted cipher algo is invalid). - // For most (but not all) cases, inputting a wrong passwords is expected to trigger this error. - return nil, errors.New("gopenpgp: wrong password in symmetric decryption") - } - - config := &packet.Config{ - Time: getTimeGenerator(), - } - - var emptyKeyRing openpgp.EntityList - md, err := openpgp.ReadMessage(encryptedIO, emptyKeyRing, prompt, config) - if err != nil { - // Parsing errors when reading the message are most likely caused by incorrect password, but we cannot know for sure - return nil, errors.New("gopenpgp: error in reading password protected message: wrong password or malformed message") - } - - messageBuf := bytes.NewBuffer(nil) - _, err = io.Copy(messageBuf, md.UnverifiedBody) - if errors.Is(err, pgpErrors.ErrMDCHashMismatch) { - // This MDC error may also be triggered if the password is correct, but the encrypted data was corrupted. - // To avoid confusion, we do not inform the user about the second possibility. - return nil, errors.New("gopenpgp: wrong password in symmetric decryption") - } - if err != nil { - // Parsing errors after decryption, triggered before parsing the MDC packet, are also usually the result of wrong password - return nil, errors.New("gopenpgp: error in reading password protected message: wrong password or malformed message") - } - - return &PlainMessage{ - Data: messageBuf.Bytes(), - TextType: !md.LiteralData.IsBinary, - Filename: md.LiteralData.FileName, - Time: md.LiteralData.Time, - }, nil -} diff --git a/crypto/sanitize_string.go b/crypto/sanitize_string.go deleted file mode 100644 index 854e11f3..00000000 --- a/crypto/sanitize_string.go +++ /dev/null @@ -1,7 +0,0 @@ -package crypto - -import "strings" - -func sanitizeString(input string) string { - return strings.ToValidUTF8(input, "\ufffd") -} diff --git a/crypto/sessionkey.go b/crypto/sessionkey.go index 6211bad5..b0a7b4be 100644 --- a/crypto/sessionkey.go +++ b/crypto/sessionkey.go @@ -1,27 +1,26 @@ package crypto import ( - "bytes" "encoding/base64" "fmt" "io" - "time" - "github.com/ProtonMail/gopenpgp/v2/constants" + "github.com/ProtonMail/gopenpgp/v3/constants" "github.com/pkg/errors" - "github.com/ProtonMail/go-crypto/openpgp" pgpErrors "github.com/ProtonMail/go-crypto/openpgp/errors" "github.com/ProtonMail/go-crypto/openpgp/packet" ) // SessionKey stores a decrypted session key. type SessionKey struct { - V6 bool - // The decrypted binary session key. + // Key defines the decrypted binary session key. Key []byte - // The symmetric encryption algorithm used with this key. + // Algo defines the symmetric encryption algorithm used with this key. + // Only present if the key was not parsed from a v6 packet. Algo string + // v6 is a flag to indicate that the session key was parsed from a v6 PKESK or SKESK packet + v6 bool } var symKeyAlgos = map[string]packet.CipherFunction{ @@ -33,6 +32,14 @@ var symKeyAlgos = map[string]packet.CipherFunction{ constants.AES256: packet.CipherAES256, } +var algosToSymKey = map[packet.CipherFunction]string{ + packet.Cipher3DES: constants.TripleDES, + packet.CipherCAST5: constants.CAST5, + packet.CipherAES128: constants.AES128, + packet.CipherAES192: constants.AES192, + packet.CipherAES256: constants.AES256, +} + type checkReader struct { decrypted io.ReadCloser body io.Reader @@ -57,9 +64,10 @@ func (cr checkReader) Read(buf []byte) (int, error) { // GetCipherFunc returns the cipher function corresponding to the algorithm used // with this SessionKey. +// Not supported in go-mobile clients use sk.GetCipherFuncInt instead. func (sk *SessionKey) GetCipherFunc() (packet.CipherFunction, error) { - if sk.V6 { - return 0, nil + if sk.v6 { + return 0, errors.New("gopenpgp: no cipher function available for a v6 session key") } cf, ok := symKeyAlgos[sk.Algo] if !ok { @@ -68,6 +76,14 @@ func (sk *SessionKey) GetCipherFunc() (packet.CipherFunction, error) { return cf, nil } +// GetCipherFuncInt returns the cipher function as int8 corresponding to the algorithm used +// with this SessionKey. +// The int8 type is used for go-mobile clients, see constant.Cipher... +func (sk *SessionKey) GetCipherFuncInt() (int8, error) { + cipherFunc, err := sk.GetCipherFunc() + return int8(cipherFunc), err +} + // GetBase64Key returns the session key as base64 encoded string. func (sk *SessionKey) GetBase64Key() string { return base64.StdEncoding.EncodeToString(sk.Key) @@ -103,15 +119,20 @@ func GenerateSessionKeyAlgo(algo string) (sk *SessionKey, err error) { } // GenerateSessionKey generates a random key for the default cipher. -func GenerateSessionKey() (*SessionKey, error) { - return GenerateSessionKeyAlgo(constants.AES256) +func generateSessionKey(config *packet.Config) (*SessionKey, error) { + cf, ok := algosToSymKey[config.DefaultCipher] + if !ok { + return nil, errors.New("gopenpgp: unsupported cipher function") + } + return GenerateSessionKeyAlgo(cf) } +// NewSessionKeyFromToken creates a SessionKey struct with the given token and algorithm. +// Clones the token for compatibility with go-mobile. func NewSessionKeyFromToken(token []byte, algo string) *SessionKey { return &SessionKey{ Key: clone(token), Algo: algo, - V6: algo == "", } } @@ -130,342 +151,20 @@ func newSessionKeyFromEncrypted(ek *packet.EncryptedKey) (*SessionKey, error) { sk := &SessionKey{ Key: ek.Key, Algo: algo, - V6: ek.Version == 6, - } - - if err := sk.checkSize(); err != nil { - return nil, errors.Wrap(err, "gopenpgp: unable to decrypt session key") - } - - return sk, nil -} - -// Encrypt encrypts a PlainMessage to PGPMessage with a SessionKey. -// * message : The plain data as a PlainMessage. -// * output : The encrypted data as PGPMessage. -func (sk *SessionKey) Encrypt(message *PlainMessage) ([]byte, error) { - return encryptWithSessionKey(message, sk, nil, false, nil) -} - -// EncryptAndSign encrypts a PlainMessage to PGPMessage with a SessionKey and signs it with a Private key. -// * message : The plain data as a PlainMessage. -// * signKeyRing: The KeyRing to sign the message -// * output : The encrypted data as PGPMessage. -func (sk *SessionKey) EncryptAndSign(message *PlainMessage, signKeyRing *KeyRing) ([]byte, error) { - return encryptWithSessionKey(message, sk, signKeyRing, false, nil) -} - -// EncryptAndSignWithContext encrypts a PlainMessage to PGPMessage with a SessionKey and signs it with a Private key. -// * message : The plain data as a PlainMessage. -// * signKeyRing: The KeyRing to sign the message -// * output : The encrypted data as PGPMessage. -// * signingContext : (optional) the context for the signature. -func (sk *SessionKey) EncryptAndSignWithContext(message *PlainMessage, signKeyRing *KeyRing, signingContext *SigningContext) ([]byte, error) { - return encryptWithSessionKey(message, sk, signKeyRing, false, signingContext) -} - -// EncryptWithCompression encrypts with compression support a PlainMessage to PGPMessage with a SessionKey. -// * message : The plain data as a PlainMessage. -// * output : The encrypted data as PGPMessage. -func (sk *SessionKey) EncryptWithCompression(message *PlainMessage) ([]byte, error) { - return encryptWithSessionKey(message, sk, nil, true, nil) -} - -func encryptWithSessionKey( - message *PlainMessage, - sk *SessionKey, - signKeyRing *KeyRing, - compress bool, - signingContext *SigningContext, -) ([]byte, error) { - var encBuf = new(bytes.Buffer) - - encryptWriter, signWriter, err := encryptStreamWithSessionKey( - NewPlainMessageMetadata( - message.IsBinary(), - message.Filename, - int64(message.Time), - ), - encBuf, - sk, - signKeyRing, - compress, - signingContext, - ) - if err != nil { - return nil, err - } - if signKeyRing != nil { - _, err = signWriter.Write(message.GetBinary()) - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: error in writing signed message") - } - err = signWriter.Close() - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: error in closing signing writer") - } - } else { - _, err = encryptWriter.Write(message.GetBinary()) - } - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: error in writing message") - } - err = encryptWriter.Close() - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: error in closing encryption writer") - } - return encBuf.Bytes(), nil -} - -func encryptStreamWithSessionKey( - plainMessageMetadata *PlainMessageMetadata, - dataPacketWriter io.Writer, - sk *SessionKey, - signKeyRing *KeyRing, - compress bool, - signingContext *SigningContext, -) (encryptWriter, signWriter io.WriteCloser, err error) { - dc, err := sk.GetCipherFunc() - if err != nil { - return nil, nil, errors.Wrap(err, "gopenpgp: unable to encrypt with session key") - } - - config := &packet.Config{ - Time: getTimeGenerator(), - DefaultCipher: dc, - } - - var signEntity *openpgp.Entity - if signKeyRing != nil { - signEntity, err = signKeyRing.getSigningEntity() - if err != nil { - return nil, nil, errors.Wrap(err, "gopenpgp: unable to sign") - } - } - - if compress { - config.DefaultCompressionAlgo = constants.DefaultCompression - config.CompressionConfig = &packet.CompressionConfig{Level: constants.DefaultCompressionLevel} + v6: ek.Version == 6, } - - if signingContext != nil { - config.SignatureNotations = append(config.SignatureNotations, signingContext.getNotation()) - } - - if plainMessageMetadata == nil { - // Use sensible default metadata - plainMessageMetadata = &PlainMessageMetadata{ - IsBinary: true, - Filename: "", - ModTime: GetUnixTime(), + if ek.Version < 6 { + if err := sk.checkSize(); err != nil { + return nil, errors.Wrap(err, "gopenpgp: unable to decrypt session key") } } - - return encryptStreamWithSessionKeyAndConfig( - plainMessageMetadata.IsBinary, - plainMessageMetadata.Filename, - uint32(plainMessageMetadata.ModTime), - dataPacketWriter, - sk, - signEntity, - config, - ) -} - -func encryptStreamWithSessionKeyAndConfig( - isBinary bool, - filename string, - modTime uint32, - dataPacketWriter io.Writer, - sk *SessionKey, - signEntity *openpgp.Entity, - config *packet.Config, -) (encryptWriter, signWriter io.WriteCloser, err error) { - encryptWriter, err = packet.SerializeSymmetricallyEncrypted( - dataPacketWriter, - config.Cipher(), - config.AEAD() != nil, - packet.CipherSuite{Cipher: config.Cipher(), Mode: config.AEAD().Mode()}, - sk.Key, - config, - ) - - if err != nil { - return nil, nil, errors.Wrap(err, "gopenpgp: unable to encrypt") - } - - if algo := config.Compression(); algo != packet.CompressionNone { - encryptWriter, err = packet.SerializeCompressed(encryptWriter, algo, config.CompressionConfig) - if err != nil { - return nil, nil, errors.Wrap(err, "gopenpgp: error in compression") - } - } - - if signEntity != nil { - hints := &openpgp.FileHints{ - IsBinary: isBinary, - FileName: filename, - ModTime: time.Unix(int64(modTime), 0), - } - - signWriter, err = openpgp.Sign(encryptWriter, signEntity, hints, config) - if err != nil { - return nil, nil, errors.Wrap(err, "gopenpgp: unable to sign") - } - } else { - encryptWriter, err = packet.SerializeLiteral( - encryptWriter, - isBinary, - filename, - modTime, - ) - if err != nil { - return nil, nil, errors.Wrap(err, "gopenpgp: unable to serialize") - } - } - return encryptWriter, signWriter, nil -} - -// Decrypt decrypts pgp data packets using directly a session key. -// * encrypted: PGPMessage. -// * output: PlainMessage. -func (sk *SessionKey) Decrypt(dataPacket []byte) (*PlainMessage, error) { - return sk.DecryptAndVerify(dataPacket, nil, 0) -} - -// DecryptAndVerify decrypts pgp data packets using directly a session key and verifies embedded signatures. -// * encrypted: PGPMessage. -// * verifyKeyRing: KeyRing with verification public keys -// * verifyTime: when should the signature be valid, as timestamp. If 0 time verification is disabled. -// * output: PlainMessage. -func (sk *SessionKey) DecryptAndVerify(dataPacket []byte, verifyKeyRing *KeyRing, verifyTime int64) (*PlainMessage, error) { - return decryptWithSessionKeyAndContext( - sk, - dataPacket, - verifyKeyRing, - verifyTime, - nil, - ) -} - -// DecryptAndVerifyWithContext decrypts pgp data packets using directly a session key and verifies embedded signatures. -// * encrypted: PGPMessage. -// * verifyKeyRing: KeyRing with verification public keys -// * verifyTime: when should the signature be valid, as timestamp. If 0 time verification is disabled. -// * output: PlainMessage. -// * verificationContext (optional): context for the signature verification. -func (sk *SessionKey) DecryptAndVerifyWithContext(dataPacket []byte, verifyKeyRing *KeyRing, verifyTime int64, verificationContext *VerificationContext) (*PlainMessage, error) { - return decryptWithSessionKeyAndContext( - sk, - dataPacket, - verifyKeyRing, - verifyTime, - verificationContext, - ) -} - -func decryptWithSessionKeyAndContext( - sk *SessionKey, - dataPacket []byte, - verifyKeyRing *KeyRing, - verifyTime int64, - verificationContext *VerificationContext, -) (*PlainMessage, error) { - var messageReader = bytes.NewReader(dataPacket) - - md, err := decryptStreamWithSessionKey(sk, messageReader, verifyKeyRing, verificationContext) - if err != nil { - return nil, err - } - messageBuf := new(bytes.Buffer) - _, err = messageBuf.ReadFrom(md.UnverifiedBody) - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: error in reading message body") - } - - if verifyKeyRing != nil { - processSignatureExpiration(md, verifyTime) - err = verifyDetailsSignature(md, verifyKeyRing, verificationContext) - } - - return &PlainMessage{ - Data: messageBuf.Bytes(), - TextType: !md.LiteralData.IsBinary, - Filename: md.LiteralData.FileName, - Time: md.LiteralData.Time, - }, err -} - -func decryptStreamWithSessionKey( - sk *SessionKey, - messageReader io.Reader, - verifyKeyRing *KeyRing, - verificationContext *VerificationContext, -) (*openpgp.MessageDetails, error) { - var decrypted io.ReadCloser - var keyring openpgp.EntityList - - // Read symmetrically encrypted data packet - packets := packet.NewReader(messageReader) - p, err := packets.Next() - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: unable to read symmetric packet") - } - - // Decrypt data packet - switch p := p.(type) { - case *packet.SymmetricallyEncrypted, *packet.AEADEncrypted: - if symPacket, ok := p.(*packet.SymmetricallyEncrypted); ok { - if !symPacket.IntegrityProtected { - return nil, errors.New("gopenpgp: message is not authenticated") - } - } - dc, err := sk.GetCipherFunc() - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: unable to decrypt with session key") - } - encryptedDataPacket, isDataPacket := p.(packet.EncryptedDataPacket) - if !isDataPacket { - return nil, errors.Wrap(err, "gopenpgp: unknown data packet") - } - decrypted, err = encryptedDataPacket.Decrypt(dc, sk.Key) - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: unable to decrypt symmetric packet") - } - default: - return nil, errors.New("gopenpgp: invalid packet type") - } - - config := &packet.Config{ - Time: getTimeGenerator(), - } - - if verificationContext != nil { - config.KnownNotations = map[string]bool{constants.SignatureContextName: true} - } - - // Push decrypted packet as literal packet and use openpgp's reader - if verifyKeyRing != nil { - keyring = verifyKeyRing.entities - } else { - keyring = openpgp.EntityList{} - } - - md, err := openpgp.ReadMessage(decrypted, keyring, nil, config) - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: unable to decode symmetric packet") - } - - md.UnverifiedBody = checkReader{decrypted, md.UnverifiedBody} - return md, nil + return sk, nil } func (sk *SessionKey) checkSize() error { - if sk.V6 { - if len(sk.Key) == 0 { - return errors.New("empty session key") - } - return nil + if sk.v6 { + // cannot check size + return errors.New("unknown key size") } cf, ok := symKeyAlgos[sk.Algo] if !ok { @@ -480,16 +179,12 @@ func (sk *SessionKey) checkSize() error { } func getAlgo(cipher packet.CipherFunction) string { - if cipher == 0 { - return "" - } - algo := constants.AES256 + algo := "" for k, v := range symKeyAlgos { if v == cipher { algo = k break } } - return algo } diff --git a/crypto/sessionkey_streaming.go b/crypto/sessionkey_streaming.go deleted file mode 100644 index 200ca639..00000000 --- a/crypto/sessionkey_streaming.go +++ /dev/null @@ -1,189 +0,0 @@ -package crypto - -import ( - "github.com/pkg/errors" -) - -type signAndEncryptWriteCloser struct { - signWriter WriteCloser - encryptWriter WriteCloser -} - -func (w *signAndEncryptWriteCloser) Write(b []byte) (int, error) { - return w.signWriter.Write(b) -} - -func (w *signAndEncryptWriteCloser) Close() error { - if err := w.signWriter.Close(); err != nil { - return err - } - return w.encryptWriter.Close() -} - -// EncryptStream is used to encrypt data as a Writer. -// It takes a writer for the encrypted data packet and returns a writer for the plaintext data. -// If signKeyRing is not nil, it is used to do an embedded signature. -func (sk *SessionKey) EncryptStream( - dataPacketWriter Writer, - plainMessageMetadata *PlainMessageMetadata, - signKeyRing *KeyRing, -) (plainMessageWriter WriteCloser, err error) { - return sk.encryptStream( - dataPacketWriter, - plainMessageMetadata, - signKeyRing, - false, - nil, - ) -} - -// EncryptStreamWithContext is used to encrypt data as a Writer. -// It takes a writer for the encrypted data packet and returns a writer for the plaintext data. -// If signKeyRing is not nil, it is used to do an embedded signature. -// * signingContext : (optional) the context for the signature. -func (sk *SessionKey) EncryptStreamWithContext( - dataPacketWriter Writer, - plainMessageMetadata *PlainMessageMetadata, - signKeyRing *KeyRing, - signingContext *SigningContext, -) (plainMessageWriter WriteCloser, err error) { - return sk.encryptStream( - dataPacketWriter, - plainMessageMetadata, - signKeyRing, - false, - signingContext, - ) -} - -// EncryptStreamWithCompression is used to encrypt data as a Writer. -// The plaintext data is compressed before being encrypted. -// It takes a writer for the encrypted data packet and returns a writer for the plaintext data. -// If signKeyRing is not nil, it is used to do an embedded signature. -// * signingContext : (optional) the context for the signature. -func (sk *SessionKey) EncryptStreamWithCompression( - dataPacketWriter Writer, - plainMessageMetadata *PlainMessageMetadata, - signKeyRing *KeyRing, -) (plainMessageWriter WriteCloser, err error) { - return sk.encryptStream( - dataPacketWriter, - plainMessageMetadata, - signKeyRing, - true, - nil, - ) -} - -// EncryptStreamWithContextAndCompression is used to encrypt data as a Writer. -// The plaintext data is compressed before being encrypted. -// It takes a writer for the encrypted data packet and returns a writer for the plaintext data. -// If signKeyRing is not nil, it is used to do an embedded signature. -// * signingContext : (optional) the context for the signature. -func (sk *SessionKey) EncryptStreamWithContextAndCompression( - dataPacketWriter Writer, - plainMessageMetadata *PlainMessageMetadata, - signKeyRing *KeyRing, - signingContext *SigningContext, -) (plainMessageWriter WriteCloser, err error) { - return sk.encryptStream( - dataPacketWriter, - plainMessageMetadata, - signKeyRing, - true, - signingContext, - ) -} - -func (sk *SessionKey) encryptStream( - dataPacketWriter Writer, - plainMessageMetadata *PlainMessageMetadata, - signKeyRing *KeyRing, - compress bool, - signingContext *SigningContext, -) (plainMessageWriter WriteCloser, err error) { - encryptWriter, signWriter, err := encryptStreamWithSessionKey( - plainMessageMetadata, - dataPacketWriter, - sk, - signKeyRing, - compress, - signingContext, - ) - - if err != nil { - return nil, err - } - if signWriter != nil { - plainMessageWriter = &signAndEncryptWriteCloser{signWriter, encryptWriter} - } else { - plainMessageWriter = encryptWriter - } - return plainMessageWriter, err -} - -// DecryptStream is used to decrypt a data packet as a Reader. -// It takes a reader for the data packet -// and returns a PlainMessageReader for the plaintext data. -// If verifyKeyRing is not nil, PlainMessageReader.VerifySignature() will -// verify the embedded signature with the given key ring and verification time. -func (sk *SessionKey) DecryptStream( - dataPacketReader Reader, - verifyKeyRing *KeyRing, - verifyTime int64, -) (plainMessage *PlainMessageReader, err error) { - return decryptStreamWithSessionKeyAndContext( - sk, - dataPacketReader, - verifyKeyRing, - verifyTime, - nil, - ) -} - -// DecryptStreamWithContext is used to decrypt a data packet as a Reader. -// It takes a reader for the data packet -// and returns a PlainMessageReader for the plaintext data. -// If verifyKeyRing is not nil, PlainMessageReader.VerifySignature() will -// verify the embedded signature with the given key ring and verification time. -// * verificationContext (optional): context for the signature verification. -func (sk *SessionKey) DecryptStreamWithContext( - dataPacketReader Reader, - verifyKeyRing *KeyRing, - verifyTime int64, - verificationContext *VerificationContext, -) (plainMessage *PlainMessageReader, err error) { - return decryptStreamWithSessionKeyAndContext( - sk, - dataPacketReader, - verifyKeyRing, - verifyTime, - verificationContext, - ) -} - -func decryptStreamWithSessionKeyAndContext( - sessionKey *SessionKey, - dataPacketReader Reader, - verifyKeyRing *KeyRing, - verifyTime int64, - verificationContext *VerificationContext, -) (plainMessage *PlainMessageReader, err error) { - messageDetails, err := decryptStreamWithSessionKey( - sessionKey, - dataPacketReader, - verifyKeyRing, - verificationContext, - ) - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: error in reading message") - } - - return &PlainMessageReader{ - messageDetails, - verifyKeyRing, - verifyTime, - false, - verificationContext, - }, err -} diff --git a/crypto/sessionkey_streaming_test.go b/crypto/sessionkey_streaming_test.go deleted file mode 100644 index 0ea9f093..00000000 --- a/crypto/sessionkey_streaming_test.go +++ /dev/null @@ -1,291 +0,0 @@ -package crypto - -import ( - "bytes" - "io" - "io/ioutil" - "reflect" - "testing" - - "github.com/pkg/errors" -) - -func TestSessionKey_EncryptDecryptStream(t *testing.T) { - messageBytes := []byte("Hello World!") - messageReader := bytes.NewReader(messageBytes) - var dataPacketBuf bytes.Buffer - messageWriter, err := testSessionKey.EncryptStream( - &dataPacketBuf, - testMeta, - keyRingTestPrivate, - ) - if err != nil { - t.Fatal("Expected no error while encrypting stream with session key, got:", err) - } - bufferSize := 2 - buffer := make([]byte, bufferSize) - reachedEnd := false - for !reachedEnd { - n, err := messageReader.Read(buffer) - if err != nil { - if errors.Is(err, io.EOF) { - reachedEnd = true - } else { - t.Fatal("Expected no error while reading data, got:", err) - } - } - writtenTotal := 0 - for writtenTotal < n { - written, err := messageWriter.Write(buffer[writtenTotal:n]) - if err != nil { - t.Fatal("Expected no error while writing data, got:", err) - } - writtenTotal += written - } - } - err = messageWriter.Close() - if err != nil { - t.Fatal("Expected no error while closing plaintext writer, got:", err) - } - dataPacket := dataPacketBuf.Bytes() - decryptedReader, err := testSessionKey.DecryptStream( - bytes.NewReader(dataPacket), - keyRingTestPublic, - GetUnixTime(), - ) - if err != nil { - t.Fatal("Expected no error while calling DecryptStream, got:", err) - } - decryptedBytes, err := ioutil.ReadAll(decryptedReader) - if err != nil { - t.Fatal("Expected no error while reading the decrypted data, got:", err) - } - err = decryptedReader.VerifySignature() - if err != nil { - t.Fatal("Expected no error while verifying the signature, got:", err) - } - if !bytes.Equal(decryptedBytes, messageBytes) { - t.Fatalf("Expected the decrypted data to be %s got %s", string(decryptedBytes), string(messageBytes)) - } - decryptedMeta := decryptedReader.GetMetadata() - if !reflect.DeepEqual(testMeta, decryptedMeta) { - t.Fatalf("Expected the decrypted metadata to be %v got %v", testMeta, decryptedMeta) - } -} - -func TestSessionKey_EncryptDecryptStreamWithContext(t *testing.T) { - messageBytes := []byte("Hello World!") - messageReader := bytes.NewReader(messageBytes) - var dataPacketBuf bytes.Buffer - testContext := "test-context" - messageWriter, err := testSessionKey.EncryptStreamWithContext( - &dataPacketBuf, - testMeta, - keyRingTestPrivate, - NewSigningContext(testContext, true), - ) - if err != nil { - t.Fatal("Expected no error while encrypting, got:", err) - } - _, err = io.Copy(messageWriter, messageReader) - if err != nil { - t.Fatal("Expected no error while copying plaintext, got:", err) - } - err = messageWriter.Close() - if err != nil { - t.Fatal("Expected no error while closing plaintext writer, got:", err) - } - dataPacket := dataPacketBuf.Bytes() - decryptedReader, err := testSessionKey.DecryptStreamWithContext( - bytes.NewReader(dataPacket), - keyRingTestPublic, - GetUnixTime(), - NewVerificationContext(testContext, true, 0), - ) - if err != nil { - t.Fatal("Expected no error while calling DecryptStream, got:", err) - } - decryptedBytes, err := ioutil.ReadAll(decryptedReader) - if err != nil { - t.Fatal("Expected no error while reading the decrypted data, got:", err) - } - err = decryptedReader.VerifySignature() - if err != nil { - t.Fatal("Expected no error while verifying the signature, got:", err) - } - if !bytes.Equal(decryptedBytes, messageBytes) { - t.Fatalf("Expected the decrypted data to be %s got %s", string(decryptedBytes), string(messageBytes)) - } - decryptedMeta := decryptedReader.GetMetadata() - if !reflect.DeepEqual(testMeta, decryptedMeta) { - t.Fatalf("Expected the decrypted metadata to be %v got %v", testMeta, decryptedMeta) - } -} - -func TestSessionKey_EncryptDecryptStreamWithContextAndCompression(t *testing.T) { - messageBytes := []byte("Hello World!") - messageReader := bytes.NewReader(messageBytes) - var dataPacketBuf bytes.Buffer - testContext := "test-context" - messageWriter, err := testSessionKey.EncryptStreamWithContextAndCompression( - &dataPacketBuf, - testMeta, - keyRingTestPrivate, - NewSigningContext(testContext, true), - ) - if err != nil { - t.Fatal("Expected no error while encrypting, got:", err) - } - _, err = io.Copy(messageWriter, messageReader) - if err != nil { - t.Fatal("Expected no error while copying plaintext, got:", err) - } - err = messageWriter.Close() - if err != nil { - t.Fatal("Expected no error while closing plaintext writer, got:", err) - } - dataPacket := dataPacketBuf.Bytes() - decryptedReader, err := testSessionKey.DecryptStreamWithContext( - bytes.NewReader(dataPacket), - keyRingTestPublic, - GetUnixTime(), - NewVerificationContext(testContext, true, 0), - ) - if err != nil { - t.Fatal("Expected no error while calling DecryptStream, got:", err) - } - decryptedBytes, err := ioutil.ReadAll(decryptedReader) - if err != nil { - t.Fatal("Expected no error while reading the decrypted data, got:", err) - } - err = decryptedReader.VerifySignature() - if err != nil { - t.Fatal("Expected no error while verifying the signature, got:", err) - } - if !bytes.Equal(decryptedBytes, messageBytes) { - t.Fatalf("Expected the decrypted data to be %s got %s", string(decryptedBytes), string(messageBytes)) - } - decryptedMeta := decryptedReader.GetMetadata() - if !reflect.DeepEqual(testMeta, decryptedMeta) { - t.Fatalf("Expected the decrypted metadata to be %v got %v", testMeta, decryptedMeta) - } -} - -func TestSessionKey_EncryptStreamCompatible(t *testing.T) { - enc := func(w io.Writer, meta *PlainMessageMetadata, kr *KeyRing) (io.WriteCloser, error) { - return testSessionKey.EncryptStream(w, meta, kr) - } - testSessionKey_EncryptStreamCompatible(enc, t) -} - -func TestSessionKey_EncryptStreamWithCompressionCompatible(t *testing.T) { - enc := func(w io.Writer, meta *PlainMessageMetadata, kr *KeyRing) (io.WriteCloser, error) { - return testSessionKey.EncryptStreamWithCompression(w, meta, kr) - } - testSessionKey_EncryptStreamCompatible(enc, t) -} - -type sessionKeyEncryptionFunction = func(io.Writer, *PlainMessageMetadata, *KeyRing) (io.WriteCloser, error) - -func testSessionKey_EncryptStreamCompatible(enc sessionKeyEncryptionFunction, t *testing.T) { - messageBytes := []byte("Hello World!") - messageReader := bytes.NewReader(messageBytes) - var dataPacketBuf bytes.Buffer - messageWriter, err := enc( - &dataPacketBuf, - testMeta, - keyRingTestPrivate, - ) - if err != nil { - t.Fatal("Expected no error while encrypting stream with session key, got:", err) - } - bufferSize := 2 - buffer := make([]byte, bufferSize) - reachedEnd := false - for !reachedEnd { - n, err := messageReader.Read(buffer) - if err != nil { - if errors.Is(err, io.EOF) { - reachedEnd = true - } else { - t.Fatal("Expected no error while reading data, got:", err) - } - } - writtenTotal := 0 - for writtenTotal < n { - written, err := messageWriter.Write(buffer[writtenTotal:n]) - if err != nil { - t.Fatal("Expected no error while writing data, got:", err) - } - writtenTotal += written - } - } - err = messageWriter.Close() - if err != nil { - t.Fatal("Expected no error while closing plaintext writer, got:", err) - } - dataPacket := dataPacketBuf.Bytes() - decryptedMsg, err := testSessionKey.DecryptAndVerify( - dataPacket, - keyRingTestPublic, - GetUnixTime(), - ) - if err != nil { - t.Fatal("Expected no error while calling DecryptAndVerify, got:", err) - } - decryptedBytes := decryptedMsg.Data - if err != nil { - t.Fatal("Expected no error while reading the decrypted data, got:", err) - } - if !bytes.Equal(decryptedBytes, messageBytes) { - t.Fatalf("Expected the decrypted data to be %s got %s", string(decryptedBytes), string(messageBytes)) - } - if testMeta.IsBinary != decryptedMsg.IsBinary() { - t.Fatalf("Expected isBinary to be %t got %t", testMeta.IsBinary, decryptedMsg.IsBinary()) - } - if testMeta.Filename != decryptedMsg.GetFilename() { - t.Fatalf("Expected filename to be %s got %s", testMeta.Filename, decryptedMsg.GetFilename()) - } - if testMeta.ModTime != int64(decryptedMsg.GetTime()) { - t.Fatalf("Expected modification time to be %d got %d", testMeta.ModTime, int64(decryptedMsg.GetTime())) - } -} - -func TestSessionKey_DecryptStreamCompatible(t *testing.T) { - messageBytes := []byte("Hello World!") - dataPacket, err := testSessionKey.EncryptAndSign( - &PlainMessage{ - Data: messageBytes, - TextType: !testMeta.IsBinary, - Time: uint32(testMeta.ModTime), - Filename: testMeta.Filename, - }, - keyRingTestPrivate, - ) - if err != nil { - t.Fatal("Expected no error while encrypting plaintext, got:", err) - } - decryptedReader, err := testSessionKey.DecryptStream( - bytes.NewReader(dataPacket), - keyRingTestPublic, - GetUnixTime(), - ) - if err != nil { - t.Fatal("Expected no error while calling DecryptStream, got:", err) - } - decryptedBytes, err := ioutil.ReadAll(decryptedReader) - if err != nil { - t.Fatal("Expected no error while reading the decrypted data, got:", err) - } - err = decryptedReader.VerifySignature() - if err != nil { - t.Fatal("Expected no error while verifying the signature, got:", err) - } - if !bytes.Equal(decryptedBytes, messageBytes) { - t.Fatalf("Expected the decrypted data to be %s got %s", string(decryptedBytes), string(messageBytes)) - } - decryptedMeta := decryptedReader.GetMetadata() - if !reflect.DeepEqual(testMeta, decryptedMeta) { - t.Fatalf("Expected the decrypted metadata to be %v got %v", testMeta, decryptedMeta) - } -} diff --git a/crypto/sessionkey_test.go b/crypto/sessionkey_test.go index f66aa65d..79ce8c2a 100644 --- a/crypto/sessionkey_test.go +++ b/crypto/sessionkey_test.go @@ -3,11 +3,10 @@ package crypto import ( "encoding/base64" "encoding/hex" - "errors" - "io/ioutil" + "os" "testing" - "github.com/ProtonMail/gopenpgp/v2/constants" + "github.com/ProtonMail/gopenpgp/v3/constants" "github.com/stretchr/testify/assert" ) @@ -15,9 +14,9 @@ var testSessionKey *SessionKey func init() { var err error - testSessionKey, err = GenerateSessionKey() + testSessionKey, err = GenerateSessionKeyAlgo("aes256") if err != nil { - panic("Expected no error while generating random session key with default algorithm, got:" + err.Error()) + panic("Expected no error while generating random session key with aes256, got:" + err.Error()) } } @@ -34,13 +33,15 @@ func TestGenerateSessionKey(t *testing.T) { } func TestAsymmetricKeyPacket(t *testing.T) { - keyPacket, err := keyRingTestPublic.EncryptSessionKey(testSessionKey) + encHandle, _ := testPGP.Encryption().Recipients(keyRingTestPublic).New() + keyPacket, err := encHandle.EncryptSessionKey(testSessionKey) if err != nil { t.Fatal("Expected no error while generating key packet, got:", err) } // Password defined in keyring_test - outputSymmetricKey, err := keyRingTestPrivate.DecryptSessionKey(keyPacket) + decHandle, _ := testPGP.Decryption().DecryptionKeys(keyRingTestPrivate).New() + outputSymmetricKey, err := decHandle.DecryptSessionKey(keyPacket) if err != nil { t.Fatal("Expected no error while decrypting key packet, got:", err) } @@ -49,13 +50,15 @@ func TestAsymmetricKeyPacket(t *testing.T) { } func TestMultipleAsymmetricKeyPacket(t *testing.T) { - keyPacket, err := keyRingTestMultiple.EncryptSessionKey(testSessionKey) + encHandle, _ := testPGP.Encryption().Recipients(keyRingTestMultiple).New() + keyPacket, err := encHandle.EncryptSessionKey(testSessionKey) if err != nil { t.Fatal("Expected no error while generating key packet, got:", err) } // Password defined in keyring_test - outputSymmetricKey, err := keyRingTestPrivate.DecryptSessionKey(keyPacket) + decHandle, _ := testPGP.Decryption().DecryptionKeys(keyRingTestPrivate).New() + outputSymmetricKey, err := decHandle.DecryptSessionKey(keyPacket) if err != nil { t.Fatal("Expected no error while decrypting key packet, got:", err) } @@ -66,19 +69,22 @@ func TestMultipleAsymmetricKeyPacket(t *testing.T) { func TestSymmetricKeyPacket(t *testing.T) { password := []byte("I like encryption") - keyPacket, err := EncryptSessionKeyWithPassword(testSessionKey, password) + encHandle, _ := testPGP.Encryption().Password(password).New() + keyPacket, err := encHandle.EncryptSessionKey(testSessionKey) if err != nil { t.Fatal("Expected no error while generating key packet, got:", err) } - wrongSymmetricKey, err := DecryptSessionKeyWithPassword(keyPacket, []byte("Wrong password")) + decHandle, _ := testPGP.Decryption().Password([]byte("Wrong password")).New() + wrongSymmetricKey, err := decHandle.DecryptSessionKey(keyPacket) if err != nil { assert.EqualError(t, err, "gopenpgp: unable to decrypt any packet") } else { assert.NotEqual(t, testSessionKey, wrongSymmetricKey) } - outputSymmetricKey, err := DecryptSessionKeyWithPassword(keyPacket, password) + decHandle, _ = testPGP.Decryption().Password(password).New() + outputSymmetricKey, err := decHandle.DecryptSessionKey(keyPacket) if err != nil { t.Fatal("Expected no error while decrypting key packet, got:", err) } @@ -98,98 +104,147 @@ func TestSymmetricKeyPacketWrongSize(t *testing.T) { password := []byte("I like encryption") - _, err = EncryptSessionKeyWithPassword(sk, password) + _, err = encryptSessionKeyWithPassword(sk, password, testPGP.profile.EncryptionConfig()) if err == nil { t.Fatal("Expected error while generating key packet with wrong sized key") } } func TestDataPacketEncryption(t *testing.T) { - var message = NewPlainMessageFromString( + var message = []byte( "The secret code is... 1, 2, 3, 4, 5. I repeat: the secret code is... 1, 2, 3, 4, 5", ) // Encrypt data with session key - dataPacket, err := testSessionKey.Encrypt(message) + encryptor, _ := testPGP.Encryption().SessionKey(testSessionKey).New() + pgpMessage, err := encryptor.Encrypt(message) if err != nil { t.Fatal("Expected no error when encrypting, got:", err) } - assert.Len(t, dataPacket, 133) // Assert uncompressed encrypted body length + assert.Len(t, pgpMessage.Bytes(), 133) // Assert uncompressed encrypted body length // Decrypt data with wrong session key - wrongKey := SessionKey{ + wrongKey := &SessionKey{ Key: []byte("wrong pass"), Algo: constants.AES256, } - _, err = wrongKey.Decrypt(dataPacket) + decryptor, _ := testPGP.Decryption().SessionKey(wrongKey).New() + _, err = decryptor.Decrypt(pgpMessage.BinaryDataPacket(), Bytes) assert.NotNil(t, err) // Decrypt data with the good session key - decrypted, err := testSessionKey.Decrypt(dataPacket) + decryptor, _ = testPGP.Decryption().SessionKey(testSessionKey).New() + decrypted, err := decryptor.Decrypt(pgpMessage.BinaryDataPacket(), Bytes) if err != nil { t.Fatal("Expected no error when decrypting, got:", err) } - assert.Exactly(t, message.GetString(), decrypted.GetString()) + assert.Exactly(t, message, decrypted.Bytes()) // Encrypt session key assert.Exactly(t, 3, len(keyRingTestMultiple.entities)) - keyPacket, err := keyRingTestMultiple.EncryptSessionKey(testSessionKey) + encryptor, _ = testPGP.Encryption().Recipients(keyRingTestMultiple).New() + keyPackets, err := encryptor.EncryptSessionKey(testSessionKey) if err != nil { t.Fatal("Unable to encrypt key packet, got:", err) } // Join key packet and data packet in single message - splitMessage := NewPGPSplitMessage(keyPacket, dataPacket) + pgpMessage.KeyPacket = keyPackets // Armor and un-armor message. In alternative it can also be done with NewPgpMessage(splitMessage.GetBinary()) - armored, err := splitMessage.GetArmored() + armored, err := pgpMessage.Armor() if err != nil { t.Fatal("Unable to armor split message, got:", err) } - pgpMessage, err := NewPGPMessageFromArmored(armored) + pgpMessage, err = NewPGPMessageFromArmored(armored) if err != nil { t.Fatal("Unable to unarmor pgp message, got:", err) } - ids, ok := pgpMessage.GetEncryptionKeyIDs() + ids, ok := pgpMessage.EncryptionKeyIDs() assert.True(t, ok) assert.Exactly(t, 3, len(ids)) // Test if final decryption succeeds - finalMessage, err := keyRingTestPrivate.Decrypt(pgpMessage, nil, 0) + decryptor, _ = testPGP.Decryption().DecryptionKeys(keyRingTestPrivate).New() + finalMessageResult, err := decryptor.Decrypt(pgpMessage.Bytes(), Bytes) if err != nil { t.Fatal("Unable to decrypt joined keypacket and datapacket, got:", err) } - assert.Exactly(t, message.GetString(), finalMessage.GetString()) + assert.Exactly(t, message, finalMessageResult.Bytes()) +} + +func TestSessionKeyClear(t *testing.T) { + testSessionKey.Clear() + assertMemCleared(t, testSessionKey.Key) +} + +func TestAEADDataPacketDecryption(t *testing.T) { + pgpMessageData, err := os.ReadFile("testdata/gpg2.3-aead-pgp-message.pgp") + if err != nil { + t.Fatal("Expected no error when reading message data, got:", err) + } + pgpMessage := NewPGPMessage(pgpMessageData) + + aeadKey, err := NewKeyFromArmored(readTestFile("gpg2.3-aead-test-key.asc", false)) + if err != nil { + t.Fatal("Expected no error when unarmoring key, got:", err) + } + + aeadKeyUnlocked, err := aeadKey.Unlock([]byte("test")) + if err != nil { + t.Fatal("Expected no error when unlocking, got:", err) + } + kR, err := NewKeyRing(aeadKeyUnlocked) + if err != nil { + t.Fatal("Expected no error when creating the keyring, got:", err) + } + defer kR.ClearPrivateParams() + decryptor, _ := testPGP.Decryption().DecryptionKeys(kR).New() + sessionKey, err := decryptor.DecryptSessionKey(pgpMessage.BinaryKeyPacket()) + if err != nil { + t.Fatal("Expected no error when decrypting session key, got:", err) + } + + decryptor, _ = testPGP.Decryption().SessionKey(sessionKey).New() + decrypted, err := decryptor.Decrypt(pgpMessage.BinaryDataPacket(), Bytes) + if err != nil { + t.Fatal("Expected no error when decrypting, got:", err) + } + + assert.Exactly(t, "hello world\n", string(decrypted.Bytes())) } func TestDataPacketEncryptionAndSignature(t *testing.T) { - var message = NewPlainMessageFromString( + var message = []byte( "The secret code is... 1, 2, 3, 4, 5. I repeat: the secret code is... 1, 2, 3, 4, 5", ) // Encrypt data with session key - dataPacket, err := testSessionKey.EncryptAndSign(message, keyRingTestPrivate) + encryptor, _ := testPGP.Encryption().SessionKey(testSessionKey).SigningKeys(keyRingTestPrivate).New() + pgpMessage, err := encryptor.Encrypt(message) if err != nil { t.Fatal("Expected no error when encrypting and signing, got:", err) } // Decrypt data with wrong session key - wrongKey := SessionKey{ + wrongKey := &SessionKey{ Key: []byte("wrong pass"), Algo: constants.AES256, } - _, err = wrongKey.Decrypt(dataPacket) + decryptor, _ := testPGP.Decryption().SessionKey(wrongKey).New() + _, err = decryptor.Decrypt(pgpMessage.BinaryDataPacket(), Bytes) assert.NotNil(t, err) // Decrypt data with the good session key - decrypted, err := testSessionKey.Decrypt(dataPacket) + decryptor, _ = testPGP.Decryption().SessionKey(testSessionKey).New() + decrypted, err := decryptor.Decrypt(pgpMessage.BinaryDataPacket(), Bytes) if err != nil { t.Fatal("Expected no error when decrypting, got:", err) } - assert.Exactly(t, message.GetString(), decrypted.GetString()) + assert.Exactly(t, message, decrypted.Bytes()) // Decrypt & verify data with the good session key but bad keyring ecKeyRing, err := NewKeyRing(keyTestEC) @@ -197,80 +252,62 @@ func TestDataPacketEncryptionAndSignature(t *testing.T) { t.Fatal("Unable to generate EC keyring, got:", err) } - castedErr := &SignatureVerificationError{} - _, err = testSessionKey.DecryptAndVerify(dataPacket, ecKeyRing, GetUnixTime()) - if err == nil || !errors.As(err, castedErr) { - t.Fatal("No error or wrong error returned for verification failure", err) + decryptor, _ = testPGP.Decryption().SessionKey(testSessionKey).VerificationKeys(ecKeyRing).New() + decrypted, err = decryptor.Decrypt(pgpMessage.BinaryDataPacket(), Bytes) + if err != nil { + t.Fatal("Wrong error returned for verification failure", err) + } + if err = decrypted.SignatureError(); err == nil { + t.Fatal("No error returned for verification failure", err) } // Decrypt & verify data with the good session key and keyring - decrypted, err = testSessionKey.DecryptAndVerify(dataPacket, keyRingTestPublic, GetUnixTime()) + decryptor, _ = testPGP.Decryption().SessionKey(testSessionKey).VerificationKeys(keyRingTestPublic).New() + decrypted, err = decryptor.Decrypt(pgpMessage.BinaryDataPacket(), Bytes) if err != nil { t.Fatal("Expected no error when decrypting & verifying, got:", err) } - assert.Exactly(t, message.GetString(), decrypted.GetString()) + if err = decrypted.SignatureError(); err != nil { + t.Fatal("Expected no error when decrypting & verifying, got:", err) + } + assert.Exactly(t, message, decrypted.Bytes()) // Encrypt session key assert.Exactly(t, 3, len(keyRingTestMultiple.entities)) - keyPacket, err := keyRingTestMultiple.EncryptSessionKey(testSessionKey) + encryptor, _ = testPGP.Encryption().Recipients(keyRingTestMultiple).New() + keyPacket, err := encryptor.EncryptSessionKey(testSessionKey) if err != nil { t.Fatal("Unable to encrypt key packet, got:", err) } // Join key packet and data packet in single message - splitMessage := NewPGPSplitMessage(keyPacket, dataPacket) + pgpMessage.KeyPacket = keyPacket // Armor and un-armor message. In alternative it can also be done with NewPgpMessage(splitMessage.GetBinary()) - armored, err := splitMessage.GetArmored() + armored, err := pgpMessage.Armor() if err != nil { t.Fatal("Unable to armor split message, got:", err) } - pgpMessage, err := NewPGPMessageFromArmored(armored) + pgpMessage, err = NewPGPMessageFromArmored(armored) if err != nil { t.Fatal("Unable to unarmor pgp message, got:", err) } - ids, ok := pgpMessage.GetEncryptionKeyIDs() + ids, ok := pgpMessage.EncryptionKeyIDs() assert.True(t, ok) assert.Exactly(t, 3, len(ids)) - // Test with bad verification key succeeds - _, err = keyRingTestPrivate.Decrypt(pgpMessage, ecKeyRing, GetUnixTime()) - if err == nil || !errors.As(err, castedErr) { - t.Fatal("No error or wrong error returned for verification failure") - } - // Test if final decryption & verification succeeds - finalMessage, err := keyRingTestPrivate.Decrypt(pgpMessage, keyRingTestPublic, GetUnixTime()) + decryptor, _ = testPGP.Decryption().DecryptionKeys(keyRingTestPrivate).VerificationKeys(keyRingTestPublic).New() + finalMessage, err := decryptor.Decrypt(pgpMessage.Bytes(), Bytes) if err != nil { t.Fatal("Unable to decrypt and verify joined keypacket and datapacket, got:", err) } - - assert.Exactly(t, message.GetString(), finalMessage.GetString()) -} - -func TestDataPacketEncryptionAndSignatureWithContext(t *testing.T) { - var message = NewPlainMessageFromString( - "The secret code is... 1, 2, 3, 4, 5. I repeat: the secret code is... 1, 2, 3, 4, 5", - ) - var testContext = "test-context" - // Encrypt data with session key - dataPacket, err := testSessionKey.EncryptAndSignWithContext(message, keyRingTestPrivate, NewSigningContext(testContext, true)) - if err != nil { - t.Fatal("Expected no error when encrypting and signing, got:", err) + if err = finalMessage.SignatureError(); err != nil { + t.Fatal("Unexpected verification error for joined keypacket and datapacket, got:", err) } - // Decrypt & verify data with the good session key and keyring - decrypted, err := testSessionKey.DecryptAndVerifyWithContext( - dataPacket, - keyRingTestPublic, - GetUnixTime(), - NewVerificationContext(testContext, true, 0), - ) - if err != nil { - t.Fatal("Expected no error when decrypting & verifying, got:", err) - } - assert.Exactly(t, message.GetString(), decrypted.GetString()) + assert.Exactly(t, message, finalMessage.Bytes()) } func TestDataPacketDecryption(t *testing.T) { @@ -278,23 +315,19 @@ func TestDataPacketDecryption(t *testing.T) { if err != nil { t.Fatal("Expected no error when unarmoring, got:", err) } - - split, err := pgpMessage.SeparateKeyAndData(1024, 0) // Test passing parameters for backwards compatibility - if err != nil { - t.Fatal("Expected no error when splitting, got:", err) - } - - sessionKey, err := keyRingTestPrivate.DecryptSessionKey(split.GetBinaryKeyPacket()) + decryptor, _ := testPGP.Decryption().DecryptionKeys(keyRingTestPrivate).New() + sessionKey, err := decryptor.DecryptSessionKey(pgpMessage.BinaryKeyPacket()) if err != nil { t.Fatal("Expected no error when decrypting session key, got:", err) } - decrypted, err := sessionKey.Decrypt(split.GetBinaryDataPacket()) + decryptor, _ = testPGP.Decryption().SessionKey(sessionKey).New() + decrypted, err := decryptor.Decrypt(pgpMessage.BinaryDataPacket(), Bytes) if err != nil { t.Fatal("Expected no error when decrypting, got:", err) } - assert.Exactly(t, readTestFile("message_plaintext", true), decrypted.GetString()) + assert.Exactly(t, readTestFile("message_plaintext", true), string(decrypted.Bytes())) } func TestMDCFailDecryption(t *testing.T) { @@ -303,45 +336,15 @@ func TestMDCFailDecryption(t *testing.T) { t.Fatal("Expected no error when unarmoring, got:", err) } - split, err := pgpMessage.SplitMessage() - if err != nil { - t.Fatal("Expected no error when splitting, got:", err) - } - sk, _ := hex.DecodeString("F76D3236E4F8A38785C50BDE7167475E95360BCE67A952710F6C16F18BB0655E") sessionKey := NewSessionKeyFromToken(sk, "aes256") - _, err = sessionKey.Decrypt(split.GetBinaryDataPacket()) + decryptor, _ := testPGP.Decryption().SessionKey(sessionKey).New() + _, err = decryptor.Decrypt(pgpMessage.BinaryDataPacket(), Bytes) assert.NotNil(t, err) } -func TestSessionKeyClear(t *testing.T) { - testSessionKey.Clear() - assertMemCleared(t, testSessionKey.Key) -} - -func TestDataPacketEncryptionWithCompression(t *testing.T) { - var message = NewPlainMessageFromString( - "The secret code is... 1, 2, 3, 4, 5. I repeat: the secret code is... 1, 2, 3, 4, 5", - ) - - // Encrypt data with session key - dataPacket, err := testSessionKey.EncryptWithCompression(message) - if err != nil { - t.Fatal("Expected no error when encrypting, got:", err) - } - - assert.Len(t, dataPacket, 117) // Assert compressed encrypted body length - - // Decrypt data with the good session key - decrypted, err := testSessionKey.Decrypt(dataPacket) - if err != nil { - t.Fatal("Expected no error when decrypting, got:", err) - } - assert.Exactly(t, message.GetString(), decrypted.GetString()) -} - func TestAsymmetricKeyPacketDecryptionFailure(t *testing.T) { passphrase := []byte("passphrase") keyPacket, err := base64.StdEncoding.DecodeString(readTestFile("sessionkey_packet", false)) @@ -366,81 +369,7 @@ func TestAsymmetricKeyPacketDecryptionFailure(t *testing.T) { t.Error("Expected no error while building private keyring, got:" + err.Error()) } - _, err = ukr.DecryptSessionKey(keyPacket) + decryptor, _ := testPGP.Decryption().DecryptionKeys(ukr).New() + _, err = decryptor.DecryptSessionKey(keyPacket) assert.Error(t, err, "gopenpgp: unable to decrypt session key") } - -func TestAEADDataPacketDecryption(t *testing.T) { - pgpMessageData, err := ioutil.ReadFile("testdata/gpg2.3-aead-pgp-message.pgp") - if err != nil { - t.Fatal("Expected no error when reading message data, got:", err) - } - pgpMessage := NewPGPMessage(pgpMessageData) - - split, err := pgpMessage.SplitMessage() - if err != nil { - t.Fatal("Expected no error when splitting, got:", err) - } - - aeadKey, err := NewKeyFromArmored(readTestFile("gpg2.3-aead-test-key.asc", false)) - if err != nil { - t.Fatal("Expected no error when unarmoring key, got:", err) - } - - aeadKeyUnlocked, err := aeadKey.Unlock([]byte("test")) - if err != nil { - t.Fatal("Expected no error when unlocking, got:", err) - } - kR, err := NewKeyRing(aeadKeyUnlocked) - if err != nil { - t.Fatal("Expected no error when creating the keyring, got:", err) - } - defer kR.ClearPrivateParams() - sessionKey, err := kR.DecryptSessionKey(split.GetBinaryKeyPacket()) - if err != nil { - t.Fatal("Expected no error when decrypting session key, got:", err) - } - - decrypted, err := sessionKey.Decrypt(split.GetBinaryDataPacket()) - if err != nil { - t.Fatal("Expected no error when decrypting, got:", err) - } - - assert.Exactly(t, "hello world\n", decrypted.GetString()) -} - -func TestSEDDecryption(t *testing.T) { - pgpMessageData, err := ioutil.ReadFile("testdata/sed_message") - if err != nil { - t.Fatal("Expected no error when reading message data, got:", err) - } - pgpMessage, err := NewPGPMessageFromArmored(string(pgpMessageData)) - if err != nil { - t.Fatal("Expected no error when creating message, got:", err) - } - - split, err := pgpMessage.SplitMessage() - if err != nil { - t.Fatal("Expected no error when splitting, got:", err) - } - - privateKey, err := NewKeyFromArmored(readTestFile("sed_key", false)) - if err != nil { - t.Fatal("Expected no error when unarmoring key, got:", err) - } - - kR, err := NewKeyRing(privateKey) - if err != nil { - t.Fatal("Expected no error when creating the keyring, got:", err) - } - defer kR.ClearPrivateParams() - sessionKey, err := kR.DecryptSessionKey(split.GetBinaryKeyPacket()) - if err != nil { - t.Fatal("Expected no error when decrypting session key, got:", err) - } - - _, err = sessionKey.Decrypt(split.GetBinaryDataPacket()) - if err == nil { - t.Fatal("sed packets without authentication should not be allowed", err) - } -} diff --git a/crypto/sign.go b/crypto/sign.go new file mode 100644 index 00000000..3d4af10d --- /dev/null +++ b/crypto/sign.go @@ -0,0 +1,25 @@ +package crypto + +import "github.com/ProtonMail/go-crypto/openpgp/packet" + +type SignProfile interface { + SignConfig() *packet.Config +} + +// PGPSign is an interface for creating signature messages with GopenPGP. +type PGPSign interface { + // SigningWriter returns a wrapper around underlying output Writer, + // such that any write-operation via the wrapper results in a write to a detached or inline signature message. + // The encoding argument defines the output encoding, i.e., Bytes or Armored + // Once close is called on the returned WriteCloser the final signature is written to the output. + // Thus, the returned WriteCloser must be closed after the plaintext has been written. + SigningWriter(output Writer, encoding int8) (WriteCloser, error) + // Sign creates a detached or inline signature from the provided byte slice. + // The encoding argument defines the output encoding, i.e., Bytes or Armored + Sign(message []byte, encoding int8) ([]byte, error) + // SignCleartext produces an armored cleartext message according to the specification. + // Returns an armored message even if the PGPSign is not configured for armored output. + SignCleartext(message []byte) ([]byte, error) + // ClearPrivateParams clears all secret key material contained in the PGPSign from memory, + ClearPrivateParams() +} diff --git a/crypto/sign_handle.go b/crypto/sign_handle.go new file mode 100644 index 00000000..70b8468f --- /dev/null +++ b/crypto/sign_handle.go @@ -0,0 +1,233 @@ +package crypto + +import ( + "bytes" + "io" + "time" + "unicode/utf8" + + "github.com/ProtonMail/go-crypto/openpgp/armor" + "github.com/ProtonMail/go-crypto/openpgp/clearsign" + "github.com/ProtonMail/go-crypto/openpgp/packet" + openpgp "github.com/ProtonMail/go-crypto/openpgp/v2" + "github.com/ProtonMail/gopenpgp/v3/constants" + "github.com/ProtonMail/gopenpgp/v3/internal" + "github.com/pkg/errors" +) + +type signatureHandle struct { + SignKeyRing *KeyRing + SignContext *SigningContext + IsUTF8 bool + Detached bool + ArmorHeaders map[string]string + profile SignProfile + clock Clock +} + +// --- Default signature handle to build from + +func defaultSignatureHandle(profile SignProfile, clock Clock) *signatureHandle { + return &signatureHandle{ + profile: profile, + ArmorHeaders: internal.ArmorHeaders, + clock: clock, + } +} + +// --- Implements the signature handle methods + +// SigningWriter returns a wrapper around underlying output Writer, +// such that any write-operation via the wrapper results in a write to a detached or inline signature message. +// The encoding argument defines the output encoding, i.e., Bytes or Armored +// Once close is called on the returned WriteCloser the final signature is written to the output. +// Thus, the returned WriteCloser must be closed after the plaintext has been written. +func (sh *signatureHandle) SigningWriter(outputWriter Writer, encoding int8) (messageWriter WriteCloser, err error) { + var armorWriter WriteCloser + armorOutput := armorOutput(encoding) + if armorOutput { + writeChecksum := sh.armorChecksumRequired() + var err error + header := constants.PGPMessageHeader + if sh.Detached { + header = constants.PGPSignatureHeader + } + armorWriter, err = armor.EncodeWithChecksumOption(outputWriter, header, sh.ArmorHeaders, writeChecksum) + if err != nil { + return nil, err + } + outputWriter = armorWriter + } + if sh.Detached { + // Detached signature + messageWriter, err = signMessageDetachedWriter( + sh.SignKeyRing, + outputWriter, + sh.IsUTF8, + sh.SignContext, + sh.clock, + sh.profile.SignConfig(), + ) + } else { + // Inline signature + messageWriter, err = sh.signingWriter(outputWriter, nil) + } + if err != nil { + return nil, err + } + if armorOutput { + // Ensure that close is called on the armor writer for the armor suffix + messageWriter = &armoredWriteCloser{ + armorWriter: armorWriter, + messageWriter: messageWriter, + } + } + if sh.IsUTF8 { + messageWriter = internal.NewUtf8CheckWriteCloser( + openpgp.NewCanonicalTextWriteCloser(messageWriter), + ) + } + return messageWriter, nil +} + +// Sign creates a detached or inline signature from the provided byte slice. +// The encoding argument defines the output encoding, i.e., Bytes or Armored. +func (sh *signatureHandle) Sign(message []byte, encoding int8) ([]byte, error) { + var writer bytes.Buffer + ptWriter, err := sh.SigningWriter(&writer, encoding) + if err != nil { + return nil, err + } + _, err = ptWriter.Write(message) + if err != nil { + return nil, err + } + err = ptWriter.Close() + if err != nil { + return nil, err + } + return writer.Bytes(), nil +} + +// SignCleartext produces an armored cleartext message according to the specification. +// Returns an armored message even if the PGPSign is not configured for armored output. +func (sh *signatureHandle) SignCleartext(message []byte) ([]byte, error) { + return sh.signCleartext(message) +} + +// ClearPrivateParams clears all secret key material contained in the PGPSign from memory. +func (sh *signatureHandle) ClearPrivateParams() { + if sh.SignKeyRing != nil { + sh.SignKeyRing.ClearPrivateParams() + } +} + +// --- Private signature handle logic + +func (sh *signatureHandle) validate() error { + if sh.SignKeyRing == nil { + return errors.New("gopenpgp: no signing key provided") + } + return nil +} + +func (sh *signatureHandle) armorChecksumRequired() bool { + if !constants.ArmorChecksumEnabled { + // If the default behavior is no checksum, we can ignore + // the logic for the crypto refresh check. + return false + } + if sh.SignKeyRing == nil { + return true + } + for _, signer := range sh.SignKeyRing.entities { + if signer.PrimaryKey.Version != 6 { + return true + } + } + return false +} + +func (sh *signatureHandle) signCleartext(message []byte) ([]byte, error) { + config := sh.profile.SignConfig() + config.Time = NewConstantClock(sh.clock().Unix()) + var buffer bytes.Buffer + var privateKeys []*packet.PrivateKey + if !utf8.Valid(message) { + return nil, internal.ErrIncorrectUtf8 + } + for _, entity := range sh.SignKeyRing.entities { + key, ok := entity.SigningKey(config.Now(), config) + if ok && + key.PrivateKey != nil && + !key.PrivateKey.Encrypted { + privateKeys = append(privateKeys, key.PrivateKey) + } else { + return nil, errors.New("gopenpgp: no signing key found for entity") + } + } + writer, err := clearsign.EncodeMultiWithHeader(&buffer, privateKeys, config, sh.ArmorHeaders) + if err != nil { + return nil, err + } + _, err = writer.Write(message) + if err != nil { + return nil, err + } + err = writer.Close() + if err != nil { + return nil, err + } + return buffer.Bytes(), nil +} + +func (sh *signatureHandle) signingWriter(messageWriter Writer, literalData *LiteralMetadata) (WriteCloser, error) { + config := sh.profile.SignConfig() + config.Time = NewConstantClock(sh.clock().Unix()) + signers, err := sh.SignKeyRing.signingEntities() + if err != nil { + return nil, err + } + hints := &openpgp.FileHints{ + FileName: literalData.Filename(), + IsUTF8: sh.IsUTF8, + ModTime: time.Unix(literalData.Time(), 0), + } + if sh.SignContext != nil { + config.SignatureNotations = append(config.SignatureNotations, sh.SignContext.getNotation()) + } + return openpgp.SignWithParams(messageWriter, signers, &openpgp.SignParams{ + Hints: hints, + TextSig: sh.IsUTF8, + Config: config, + }) +} + +func signMessageDetachedWriter( + signKeyRing *KeyRing, + outputWriter io.Writer, + isUTF8 bool, + context *SigningContext, + clock Clock, + config *packet.Config, +) (ptWriter io.WriteCloser, err error) { + config.Time = NewConstantClock(clock().Unix()) + + signers, err := signKeyRing.signingEntities() + if err != nil { + return nil, err + } + + if context != nil { + config.SignatureNotations = append(config.SignatureNotations, context.getNotation()) + } + + ptWriter, err = openpgp.DetachSignWriter(outputWriter, signers, &openpgp.SignParams{ + TextSig: isUTF8, + Config: config, + }) + if err != nil { + return nil, errors.Wrap(err, "gopenpgp: error in signing") + } + return ptWriter, nil +} diff --git a/crypto/sign_handle_builder.go b/crypto/sign_handle_builder.go new file mode 100644 index 00000000..f4460756 --- /dev/null +++ b/crypto/sign_handle_builder.go @@ -0,0 +1,97 @@ +package crypto + +// SignHandleBuilder allows to configure a sign handle +// to sign data with OpenPGP. +type SignHandleBuilder struct { + handle *signatureHandle + defaultClock Clock + err error +} + +func newSignHandleBuilder(profile SignProfile, clock Clock) *SignHandleBuilder { + return &SignHandleBuilder{ + handle: defaultSignatureHandle(profile, clock), + defaultClock: clock, + } +} + +// SigningKey sets the signing key that is used to create signature of the message. +func (shb *SignHandleBuilder) SigningKey(key *Key) *SignHandleBuilder { + var err error + if shb.handle.SignKeyRing == nil { + shb.handle.SignKeyRing, err = NewKeyRing(key) + } else { + err = shb.handle.SignKeyRing.AddKey(key) + } + shb.err = err + return shb +} + +// SigningKeys sets the signing keys that are used to create signature of the message. +func (shb *SignHandleBuilder) SigningKeys(signingKeys *KeyRing) *SignHandleBuilder { + shb.handle.SignKeyRing = signingKeys + return shb +} + +// SigningContext provides a signing context for the signature in the message. +// Triggers that each signature includes the sining context. +func (shb *SignHandleBuilder) SigningContext(signingContext *SigningContext) *SignHandleBuilder { + shb.handle.SignContext = signingContext + return shb +} + +// Detached indicates if a detached signature should be produced. +// The sign output will be a detached signature message without the data included. +func (shb *SignHandleBuilder) Detached() *SignHandleBuilder { + shb.handle.Detached = true + return shb +} + +// ArmorHeader indicates that the produced signature should be armored +// with the given version and comment as header. +// Note that this option only affects the method SignHandle.SigningWriter +// and the headers in SignHandle.SignCleartext. +func (shb *SignHandleBuilder) ArmorHeader(version, comment string) *SignHandleBuilder { + if shb.handle.ArmorHeaders == nil { + shb.handle.ArmorHeaders = make(map[string]string) + } + shb.handle.ArmorHeaders["Version"] = version + shb.handle.ArmorHeaders["Comment"] = comment + return shb +} + +// Utf8 indicates if the plaintext should be signed with a text type +// signature. If set, the plaintext is signed after +// canonicalising the line endings. +func (shb *SignHandleBuilder) Utf8() *SignHandleBuilder { + shb.handle.IsUTF8 = true + return shb +} + +// SignTime sets the internal clock to always return +// the supplied unix time for signing instead of the device time. +func (shb *SignHandleBuilder) SignTime(unixTime int64) *SignHandleBuilder { + shb.handle.clock = NewConstantClock(unixTime) + return shb +} + +// New creates a SignHandle and checks that the given +// combination of parameters is valid. If the parameters are invalid +// an error is returned. +func (shb *SignHandleBuilder) New() (PGPSign, error) { + if shb.err != nil { + return nil, shb.err + } + shb.err = shb.handle.validate() + if shb.err != nil { + return nil, shb.err + } + handle := shb.handle + shb.handle = defaultSignatureHandle(shb.handle.profile, shb.defaultClock) + return handle, nil +} + +// Error returns any errors that occurred within the builder. +func (shb *SignHandleBuilder) Error() error { + return shb.err +} diff --git a/crypto/sign_verify_test.go b/crypto/sign_verify_test.go new file mode 100644 index 00000000..b9289ed9 --- /dev/null +++ b/crypto/sign_verify_test.go @@ -0,0 +1,346 @@ +package crypto + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" +) + +const messageToSign = "Hello World!" +const messageCleartext = " Signed message\n \n " +const expectedMessageCleartext = " Signed message\n\n" + +func TestSignVerifyStream(t *testing.T) { + for _, material := range testMaterialForProfiles { + t.Run(material.profileName, func(t *testing.T) { + signer, _ := material.pgp.Sign(). + SigningKeys(material.keyRingTestPrivate). + New() + verifier, _ := material.pgp.Verify(). + VerificationKeys(material.keyRingTestPublic). + New() + testSignVerifyStream(t, signer, verifier, Bytes, len(material.keyRingTestPrivate.entities)) + }) + } +} + +func TestSignVerifyStreamContext(t *testing.T) { + for _, material := range testMaterialForProfiles { + t.Run(material.profileName, func(t *testing.T) { + signer, _ := material.pgp.Sign(). + SigningKeys(material.keyRingTestPrivate). + SigningContext(NewSigningContext(testContext, true)). + New() + verifier, _ := material.pgp.Verify(). + VerificationKeys(material.keyRingTestPublic). + VerificationContext(NewVerificationContext(testContext, true, 0)). + New() + testSignVerifyStream(t, signer, verifier, Bytes, len(material.keyRingTestPrivate.entities)) + }) + } +} + +func TestSignVerifyStreamArmor(t *testing.T) { + for _, material := range testMaterialForProfiles { + t.Run(material.profileName, func(t *testing.T) { + signer, _ := material.pgp.Sign(). + SigningKeys(material.keyRingTestPrivate). + New() + verifier, _ := material.pgp.Verify(). + VerificationKeys(material.keyRingTestPublic). + New() + testSignVerifyStream(t, signer, verifier, Armor, len(material.keyRingTestPrivate.entities)) + }) + } +} + +func TestSignVerify(t *testing.T) { + for _, material := range testMaterialForProfiles { + t.Run(material.profileName, func(t *testing.T) { + signer, _ := material.pgp.Sign(). + SigningKeys(material.keyRingTestPrivate). + New() + verifier, _ := material.pgp.Verify(). + VerificationKeys(material.keyRingTestPublic). + New() + testSignVerify(t, signer, verifier, false, Bytes, len(material.keyRingTestPrivate.entities)) + }) + } +} + +func TestSignVerifyUtf8(t *testing.T) { + for _, material := range testMaterialForProfiles { + t.Run(material.profileName, func(t *testing.T) { + signer, _ := material.pgp.Sign(). + SigningKeys(material.keyRingTestPrivate). + Utf8(). + New() + verifier, _ := material.pgp.Verify(). + VerificationKeys(material.keyRingTestPublic). + Utf8(). + New() + testSignVerify(t, signer, verifier, false, Bytes, len(material.keyRingTestPrivate.entities)) + }) + } +} + +func TestSignVerifyDetached(t *testing.T) { + for _, material := range testMaterialForProfiles { + t.Run(material.profileName, func(t *testing.T) { + signer, _ := material.pgp.Sign(). + SigningKeys(material.keyRingTestPrivate). + Detached(). + New() + verifier, _ := material.pgp.Verify(). + VerificationKeys(material.keyRingTestPublic). + New() + testSignVerify(t, signer, verifier, true, Bytes, len(material.keyRingTestPrivate.entities)) + }) + } +} + +func TestSignVerifyDetachedUtf8(t *testing.T) { + for _, material := range testMaterialForProfiles { + t.Run(material.profileName, func(t *testing.T) { + signer, _ := material.pgp.Sign(). + SigningKeys(material.keyRingTestPrivate). + Detached(). + Utf8(). + New() + verifier, _ := material.pgp.Verify(). + VerificationKeys(material.keyRingTestPublic). + Utf8(). + New() + testSignVerify(t, signer, verifier, true, Bytes, len(material.keyRingTestPrivate.entities)) + }) + } +} + +func TestSignVerifyStreamDetached(t *testing.T) { + for _, material := range testMaterialForProfiles { + t.Run(material.profileName, func(t *testing.T) { + signer, _ := material.pgp.Sign(). + SigningKeys(material.keyRingTestPrivate). + Detached(). + New() + verifier, _ := material.pgp.Verify(). + VerificationKeys(material.keyRingTestPublic). + New() + testSignVerifyDetachedStream(t, signer, verifier, Bytes, len(material.keyRingTestPrivate.entities)) + }) + } +} + +func TestSignVerifyStreamDetachedContext(t *testing.T) { + for _, material := range testMaterialForProfiles { + t.Run(material.profileName, func(t *testing.T) { + signer, _ := material.pgp.Sign(). + SigningKeys(material.keyRingTestPrivate). + Detached(). + SigningContext(NewSigningContext(testContext, true)). + New() + verifier, _ := material.pgp.Verify(). + VerificationKeys(material.keyRingTestPublic). + VerificationContext(NewVerificationContext(testContext, true, 0)). + New() + testSignVerifyDetachedStream(t, signer, verifier, Bytes, len(material.keyRingTestPrivate.entities)) + }) + } +} + +func TestSignVerifyStreamDetachedArmor(t *testing.T) { + for _, material := range testMaterialForProfiles { + t.Run(material.profileName, func(t *testing.T) { + signer, _ := material.pgp.Sign(). + SigningKeys(material.keyRingTestPrivate). + Detached(). + New() + verifier, _ := material.pgp.Verify(). + VerificationKeys(material.keyRingTestPublic). + New() + testSignVerifyDetachedStream(t, signer, verifier, Armor, len(material.keyRingTestPrivate.entities)) + }) + } +} + +func TestSignVerifyCleartext(t *testing.T) { + for _, material := range testMaterialForProfiles { + t.Run(material.profileName, func(t *testing.T) { + signer, _ := material.pgp.Sign(). + SigningKeys(material.keyRingTestPrivate). + New() + verifier, _ := material.pgp.Verify(). + VerificationKeys(material.keyRingTestPublic). + New() + testSignVerifyCleartext(t, signer, verifier) + }) + } +} + +func TestSignArmor(t *testing.T) { + for _, material := range testMaterialForProfiles { + t.Run(material.profileName, func(t *testing.T) { + isV6 := material.keyRingTestPrivate.GetKeys()[0].isV6() + signer, _ := material.pgp.Sign(). + SigningKeys(material.keyRingTestPrivate). + New() + armoredSignature, err := signer.Sign([]byte(testMessageString), Armor) + if err != nil { + t.Fatal("Expected no error in singing, got:", err) + } + hasChecksum := containsChecksum(string(armoredSignature)) + if isV6 && hasChecksum { + t.Fatalf("V6 messages should not have a checksum") + } + }) + } +} + +func testSignVerify( + t *testing.T, + signer PGPSign, + verifier PGPVerify, + detached bool, + encoding int8, //nolint:unparam + numberOfSigsToVerify int, +) { + messageBytes := []byte(messageToSign) + signature, err := signer.Sign(messageBytes, encoding) + if err != nil { + t.Fatal("Expected no error while signing the message, got:", err) + } + var verifyResult *VerifyResult + if detached { + verifyResult, err = verifier.VerifyDetached(messageBytes, signature, encoding) + } else { + verifyDataResult, err := verifier.VerifyInline(signature, encoding) + if err != nil { + t.Fatal("Expected no error while verifying the message, got:", err) + } + if !bytes.Equal(messageBytes, verifyDataResult.Bytes()) { + t.Fatal("Expected read message in verification to be equal to the input message") + } + verifyResult = &verifyDataResult.VerifyResult + } + if err != nil { + t.Fatal("Expected no error while verifying the message, got:", err) + } + if err = verifyResult.SignatureError(); err != nil { + t.Fatal("Expected no error while verifying the detached signature, got:", err) + } + if len(verifyResult.Signatures) != numberOfSigsToVerify { + t.Fatalf("Not enough signatures verified, should be %d", numberOfSigsToVerify) + } + for _, verifiedSignature := range verifyResult.Signatures { + if verifiedSignature.SignatureError != nil { + t.Fatal("One of the contained signatures did not correctly verify ", verifiedSignature.SignatureError.Message) + } + } +} + +func testSignVerifyStream( + t *testing.T, + signer PGPSign, + verifier PGPVerify, + encoding int8, + numberOfSigsToVerify int, +) { + messageBytes := []byte(messageToSign) + var messageBuffer bytes.Buffer + signingWriter, err := signer.SigningWriter(&messageBuffer, encoding) + if err != nil { + t.Fatal("Expected no error while signing the message, got:", err) + } + _, err = signingWriter.Write(messageBytes) + if err != nil { + t.Fatal("Expected no error while writing message, got:", err) + } + err = signingWriter.Close() + if err != nil { + t.Fatal("Expected no error while sining message, got:", err) + } + + verifyingReader, err := verifier.VerifyingReader(nil, &messageBuffer, encoding) + if err != nil { + t.Fatal("Expected no error while verifying the message, got:", err) + } + messageOut, err := verifyingReader.ReadAll() + if err != nil { + t.Fatal("Expected no error while verifying the message, got:", err) + } + verifyResult, err := verifyingReader.VerifySignature() + if err != nil { + t.Fatal("Expected no error while verifying the message, got:", err) + } + if err = verifyResult.SignatureError(); err != nil { + t.Fatal("Expected no error while verifying the detached signature, got:", err) + } + if !bytes.Equal(messageOut, messageBytes) { + t.Fatal("Expected read message in verification to be equal to the input message") + } + if len(verifyResult.Signatures) != numberOfSigsToVerify { + t.Fatalf("Not enough signatures verified, should be %d", numberOfSigsToVerify) + } + for _, verifiedSignature := range verifyResult.Signatures { + if verifiedSignature.SignatureError != nil { + t.Fatal("One of the contained signatures did not correctly verify ", verifiedSignature.SignatureError.Message) + } + } +} + +func testSignVerifyDetachedStream( + t *testing.T, + signer PGPSign, + verifier PGPVerify, + encoding int8, + numberOfSigsToVerify int, +) { + messageBytes := []byte(messageCleartext) + var signatureBuffer bytes.Buffer + signingWriter, err := signer.SigningWriter(&signatureBuffer, encoding) + if err != nil { + t.Fatal("Expected no error while signing the message, got:", err) + } + _, err = signingWriter.Write(messageBytes) + if err != nil { + t.Fatal("Expected no error while writing message, got:", err) + } + err = signingWriter.Close() + if err != nil { + t.Fatal("Expected no error while sining message, got:", err) + } + + verifyingReader, _ := verifier.VerifyingReader(bytes.NewReader(messageBytes), &signatureBuffer, encoding) + verifyResult, err := verifyingReader.DiscardAllAndVerifySignature() + if err != nil { + t.Fatal("Expected no error while verifying the message, got:", err) + } + if err = verifyResult.SignatureError(); err != nil { + t.Fatal("Expected no error while verifying the detached signature, got:", err) + } + if len(verifyResult.Signatures) != numberOfSigsToVerify { + t.Fatalf("Not enough signatures verified, should be %d", numberOfSigsToVerify) + } + for _, verifiedSignature := range verifyResult.Signatures { + if verifiedSignature.SignatureError != nil { + t.Fatal("One of the contained signatures did not correctly verify ", verifiedSignature.SignatureError.Message) + } + } +} + +func testSignVerifyCleartext(t *testing.T, signer PGPSign, verifier PGPVerify) { + messageBytes := []byte(messageCleartext) + cleartextMessage, err := signer.SignCleartext(messageBytes) + if err != nil { + t.Fatal("Expected no error while signing the message, got:", err) + } + result, err := verifier.VerifyCleartext(cleartextMessage) + if err != nil { + t.Fatal("Expected no error while verifying the message, got:", err) + } + if err = result.SignatureError(); err != nil { + t.Fatal("Expected no signature error while verifying the detached signature, got:", err) + } + assert.Exactly(t, expectedMessageCleartext, string(result.Cleartext())) +} diff --git a/crypto/signature.go b/crypto/signature.go index f5c58ab3..a559a0ab 100644 --- a/crypto/signature.go +++ b/crypto/signature.go @@ -4,24 +4,28 @@ import ( "bytes" "crypto" "fmt" - "io" - "math" "time" - "github.com/ProtonMail/go-crypto/openpgp" pgpErrors "github.com/ProtonMail/go-crypto/openpgp/errors" "github.com/ProtonMail/go-crypto/openpgp/packet" + openpgp "github.com/ProtonMail/go-crypto/openpgp/v2" "github.com/pkg/errors" - "github.com/ProtonMail/gopenpgp/v2/constants" - "github.com/ProtonMail/gopenpgp/v2/internal" + "github.com/ProtonMail/gopenpgp/v3/constants" ) -var allowedHashes = []crypto.Hash{ - crypto.SHA224, - crypto.SHA256, - crypto.SHA384, - crypto.SHA512, +var allowedHashesSet = map[crypto.Hash]struct{}{ + crypto.SHA224: {}, + crypto.SHA256: {}, + crypto.SHA384: {}, + crypto.SHA512: {}, +} + +// VerifiedSignature is a result of a signature verification. +type VerifiedSignature struct { + Signature *packet.Signature + SignedBy *Key + SignatureError *SignatureVerificationError } // SignatureVerificationError is returned from Decrypt and VerifyDetached @@ -32,6 +36,134 @@ type SignatureVerificationError struct { Cause error } +// VerifyResult is a result of a pgp message signature verification. +type VerifyResult struct { + // All signatures found in the message. + Signatures []*VerifiedSignature + // The selected signature for the result. + // i.e., the first successfully verified signature in Signatures + // or the last signature Signatures[len(Signatures)-1]. + selectedSignature *VerifiedSignature + // The signature error of the selected signature. + // Is nil for a successful verification. + signatureError *SignatureVerificationError +} + +// SignatureCreationTime returns the creation time of +// the selected verified signature if found, else returns 0. +func (vr *VerifyResult) SignatureCreationTime() int64 { + if vr.selectedSignature == nil || vr.selectedSignature.Signature == nil { + return 0 + } + return vr.selectedSignature.Signature.CreationTime.Unix() +} + +// SignedWithType returns the type of the signature if found, else returns 0. +// Not supported in go-mobile use SignedWithTypeInteger instead. +func (vr *VerifyResult) SignedWithType() packet.SignatureType { + if vr.selectedSignature == nil || vr.selectedSignature.Signature == nil { + return 0 + } + return vr.selectedSignature.Signature.SigType +} + +// SignedWithTypeInt8 returns the type of the signature as int8 type if found, else returns 0. +// See constants.SigType... for the different types. +func (vr *VerifyResult) SignedWithTypeInt8() int8 { + return int8(vr.SignedWithType()) +} + +// SignedByKeyId returns the key id of the key that was used to verify the selected signature, +// if found, else returns 0. +// Not supported in go-mobile use SignedByKeyIdString instead. +func (vr *VerifyResult) SignedByKeyId() uint64 { + if vr.selectedSignature == nil || vr.selectedSignature.Signature == nil { + return 0 + } + return *vr.selectedSignature.Signature.IssuerKeyId +} + +// SignedByKeyIdHex returns the key id of the key that was used to verify the selected signature +// as a hex encoded string. +// Helper for go-mobile. +func (vr *VerifyResult) SignedByKeyIdHex() string { + return keyIDToHex(vr.SignedByKeyId()) +} + +// SignedByFingerprint returns the key fingerprint of the key that was used to verify the selected signature, +// if found, else returns nil. +func (vr *VerifyResult) SignedByFingerprint() []byte { + if vr.selectedSignature == nil || vr.selectedSignature.Signature == nil { + return nil + } + if vr.selectedSignature.Signature.IssuerFingerprint != nil { + return vr.selectedSignature.Signature.IssuerFingerprint + } + if vr.selectedSignature.SignedBy != nil { + return vr.selectedSignature.SignedBy.GetFingerprintBytes() + } + return nil +} + +// SignedByKey returns the key that was used to verify the selected signature, +// if found, else returns nil. +func (vr *VerifyResult) SignedByKey() *Key { + if vr.selectedSignature == nil || vr.selectedSignature.Signature == nil { + return nil + } + key := vr.selectedSignature.SignedBy + if key == nil { + return nil + } + return &Key{ + entity: key.entity, + } +} + +// Signature returns the serialized openpgp signature packet of the selected signature. +func (vr *VerifyResult) Signature() ([]byte, error) { + if vr.selectedSignature == nil || vr.selectedSignature.Signature == nil { + return nil, errors.New("gopenpgp: no signature present") + } + var serializedSignature bytes.Buffer + if err := vr.selectedSignature.Signature.Serialize(&serializedSignature); err != nil { + return nil, errors.Wrap(err, "gopenpgp: signature serialization failed") + } + return serializedSignature.Bytes(), nil +} + +// SignatureError returns nil if no signature err occurred else +// the signature error. +func (vr *VerifyResult) SignatureError() error { + if vr == nil || vr.signatureError == nil { + return nil + } + return *vr.signatureError +} + +// SignatureErrorExplicit returns nil if no signature err occurred else +// the explicit signature error. +func (vr *VerifyResult) SignatureErrorExplicit() *SignatureVerificationError { + return vr.signatureError +} + +// ConstrainToTimeRange updates the signature result to only consider +// signatures with a creation time within the given time frame. +// unixFrom and unixTo are in unix time and are inclusive. +func (vr *VerifyResult) ConstrainToTimeRange(unixFrom int64, unixTo int64) { + for _, signature := range vr.Signatures { + if signature.Signature != nil && signature.SignatureError == nil { + sigUnixTime := signature.Signature.CreationTime.Unix() + if sigUnixTime < unixFrom || sigUnixTime > unixTo { + sigError := newSignatureFailed(errors.New("gopenpgp: signature creation time is out of range")) + signature.SignatureError = &sigError + } + } + } + // Reselect + vr.selectSignature() +} + // Error is the base method for all errors. func (e SignatureVerificationError) Error() string { if e.Cause != nil { @@ -49,6 +181,30 @@ func (e SignatureVerificationError) Unwrap() error { // Internal functions // ------------------ +// selectSignature selects the main signature to show in the result. +// Selection policy: +// first successfully verified or +// last signature with an error and a matching key or +// last signature with an error if no key matched. +func (vr *VerifyResult) selectSignature() { + var keyMatch bool + for _, signature := range vr.Signatures { + if signature.SignedBy != nil { + keyMatch = true + vr.selectedSignature = signature + vr.signatureError = signature.SignatureError + if signature.SignatureError == nil { + break + } + } + } + if !keyMatch && len(vr.Signatures) > 0 { + signature := vr.Signatures[len(vr.Signatures)-1] + vr.selectedSignature = signature + vr.signatureError = signature.SignatureError + } +} + // newSignatureFailed creates a new SignatureVerificationError, type // SignatureFailed. func newSignatureBadContext(cause error) SignatureVerificationError { @@ -95,52 +251,81 @@ func newSignatureNoVerifier() SignatureVerificationError { } // processSignatureExpiration handles signature time verification manually, so -// we can add a margin to the creationTime check. -func processSignatureExpiration(md *openpgp.MessageDetails, verifyTime int64) { - if !errors.Is(md.SignatureError, pgpErrors.ErrSignatureExpired) { - return +// we can ignore signature expired errors if configured so. +func processSignatureExpiration(sig *packet.Signature, toCheck error, verifyTime int64, disableTimeCheck bool) error { + if sig == nil || !errors.Is(toCheck, pgpErrors.ErrSignatureExpired) { + return toCheck } - if verifyTime == 0 { - // verifyTime = 0: time check disabled, everything is okay - md.SignatureError = nil - return - } - created := md.Signature.CreationTime.Unix() - expires := int64(math.MaxInt64) - if md.Signature.SigLifetimeSecs != nil { - expires = int64(*md.Signature.SigLifetimeSecs) + created - } - if created-internal.CreationTimeOffset <= verifyTime && verifyTime <= expires { - md.SignatureError = nil + if disableTimeCheck || verifyTime == 0 { + return nil } + return toCheck } -// verifyDetailsSignature verifies signature from message details. -func verifyDetailsSignature(md *openpgp.MessageDetails, verifierKey *KeyRing, verificationContext *VerificationContext) error { +func createVerifyResult( + md *openpgp.MessageDetails, + verifierKey *KeyRing, + verificationContext *VerificationContext, + verifyTime int64, + disableTimeCheck bool, +) (*VerifyResult, error) { if !md.IsSigned { - return newSignatureNotSigned() - } - if md.SignedBy == nil || - len(verifierKey.entities) == 0 || - len(verifierKey.entities.KeysById(md.SignedByKeyId)) == 0 { - return newSignatureNoVerifier() - } - if md.SignatureError != nil { - return newSignatureFailed(md.SignatureError) + signatureError := newSignatureNotSigned() + return &VerifyResult{ + signatureError: &signatureError, + }, nil } - if md.Signature == nil || - md.Signature.Hash < allowedHashes[0] || - md.Signature.Hash > allowedHashes[len(allowedHashes)-1] { - return newSignatureInsecure() + if !md.IsVerified { + return nil, errors.New("gopenpgp: message has not been verified") } - if verificationContext != nil { - err := verificationContext.verifyContext(md.Signature) - if err != nil { - return newSignatureBadContext(err) + + verifiedSignatures := make([]*VerifiedSignature, len(md.SignatureCandidates)) + for candidateIndex, signature := range md.SignatureCandidates { + var singedBy *Key + if signature.SignedBy != nil { + singedBy = &Key{ + entity: signature.SignedBy.Entity, + } + } + verifiedSignature := &VerifiedSignature{ + Signature: signature.CorrespondingSig, + SignedBy: singedBy, + } + signature.SignatureError = processSignatureExpiration( + signature.CorrespondingSig, + signature.SignatureError, + verifyTime, + disableTimeCheck, + ) + var signatureError SignatureVerificationError + + switch { + case verifierKey == nil || len(verifierKey.entities) == 0 || + errors.Is(signature.SignatureError, pgpErrors.ErrUnknownIssuer): + signatureError = newSignatureNoVerifier() + case signature.SignatureError != nil: + signatureError = newSignatureFailed(signature.SignatureError) + case signature.CorrespondingSig == nil || !isHashAllowed(signature.CorrespondingSig.Hash): + signatureError = newSignatureInsecure() + case verificationContext != nil: + err := verificationContext.verifyContext(signature.CorrespondingSig) + if err != nil { + signatureError = newSignatureBadContext(err) + } + } + if signatureError.Status != constants.SIGNATURE_OK { + verifiedSignature.SignatureError = &signatureError } + verifiedSignatures[candidateIndex] = verifiedSignature } - return nil + verifyResult := &VerifyResult{ + Signatures: verifiedSignatures, + } + + // Select the signature to show in the result + verifyResult.selectSignature() + return verifyResult, nil } // SigningContext gives the context that will be @@ -225,108 +410,7 @@ func (context *VerificationContext) verifyContext(sig *packet.Signature) error { return nil } -// verifySignature verifies if a signature is valid with the entity list. -func verifySignature( - pubKeyEntries openpgp.EntityList, - origText io.Reader, - signature []byte, - verifyTime int64, - verificationContext *VerificationContext, -) (*packet.Signature, error) { - config := &packet.Config{} - if verifyTime == 0 { - config.Time = func() time.Time { - return time.Unix(0, 0) - } - } else { - config.Time = func() time.Time { - return time.Unix(verifyTime+internal.CreationTimeOffset, 0) - } - } - - if verificationContext != nil { - config.KnownNotations = map[string]bool{constants.SignatureContextName: true} - } - signatureReader := bytes.NewReader(signature) - - sig, signer, err := openpgp.VerifyDetachedSignatureAndHash(pubKeyEntries, origText, signatureReader, allowedHashes, config) - - if sig != nil && signer != nil && (errors.Is(err, pgpErrors.ErrSignatureExpired) || errors.Is(err, pgpErrors.ErrKeyExpired)) { //nolint:nestif - if verifyTime == 0 { // Expiration check disabled - err = nil - } else { - // Maybe the creation time offset pushed it over the edge - // Retry with the actual verification time - config.Time = func() time.Time { - return time.Unix(verifyTime, 0) - } - - seeker, ok := origText.(io.ReadSeeker) - if !ok { - return nil, errors.Wrap(err, "gopenpgp: message reader do not support seeking, cannot retry signature verification") - } - - _, err = seeker.Seek(0, io.SeekStart) - if err != nil { - return nil, newSignatureFailed(errors.Wrap(err, "gopenpgp: could not rewind the data reader.")) - } - - _, err = signatureReader.Seek(0, io.SeekStart) - if err != nil { - return nil, newSignatureFailed(err) - } - - sig, signer, err = openpgp.VerifyDetachedSignatureAndHash(pubKeyEntries, seeker, signatureReader, allowedHashes, config) - } - } - - if err != nil { - return nil, newSignatureFailed(err) - } - - if sig == nil || signer == nil { - return nil, newSignatureFailed(errors.New("gopenpgp: no signer or valid signature")) - } - - if verificationContext != nil { - err := verificationContext.verifyContext(sig) - if err != nil { - return nil, newSignatureBadContext(err) - } - } - - return sig, nil -} - -func signMessageDetached( - signKeyRing *KeyRing, - messageReader io.Reader, - isBinary bool, - context *SigningContext, -) (*PGPSignature, error) { - config := &packet.Config{ - DefaultHash: crypto.SHA512, - Time: getTimeGenerator(), - } - - signEntity, err := signKeyRing.getSigningEntity() - if err != nil { - return nil, err - } - - if context != nil { - config.SignatureNotations = append(config.SignatureNotations, context.getNotation()) - } - - var outBuf bytes.Buffer - if isBinary { - err = openpgp.DetachSign(&outBuf, signEntity, messageReader, config) - } else { - err = openpgp.DetachSignText(&outBuf, signEntity, messageReader, config) - } - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: error in signing") - } - - return NewPGPSignature(outBuf.Bytes()), nil +func isHashAllowed(h crypto.Hash) bool { + _, ok := allowedHashesSet[h] + return ok } diff --git a/crypto/signature_collector.go b/crypto/signature_collector.go deleted file mode 100644 index fb769a91..00000000 --- a/crypto/signature_collector.go +++ /dev/null @@ -1,125 +0,0 @@ -package crypto - -import ( - "bytes" - "io" - "io/ioutil" - "mime" - "net/textproto" - - pgpErrors "github.com/ProtonMail/go-crypto/openpgp/errors" - "github.com/ProtonMail/gopenpgp/v2/internal" - - "github.com/ProtonMail/go-crypto/openpgp" - "github.com/ProtonMail/go-crypto/openpgp/packet" - gomime "github.com/ProtonMail/go-mime" - "github.com/pkg/errors" -) - -// SignatureCollector structure. -type SignatureCollector struct { - config *packet.Config - keyring openpgp.KeyRing - target gomime.VisitAcceptor - signature string - verified error -} - -func newSignatureCollector( - targetAcceptor gomime.VisitAcceptor, keyring openpgp.KeyRing, config *packet.Config, -) *SignatureCollector { - return &SignatureCollector{ - target: targetAcceptor, - config: config, - keyring: keyring, - } -} - -// Accept collects the signature. -func (sc *SignatureCollector) Accept( - part io.Reader, header textproto.MIMEHeader, - hasPlainSibling, isFirst, isLast bool, -) (err error) { - parentMediaType, params, _ := mime.ParseMediaType(header.Get("Content-Type")) - - if parentMediaType != "multipart/signed" { - sc.verified = newSignatureNotSigned() - return sc.target.Accept(part, header, hasPlainSibling, isFirst, isLast) - } - - newPart, rawBody := gomime.GetRawMimePart(part, "--"+params["boundary"]) - multiparts, multipartHeaders, err := gomime.GetMultipartParts(newPart, params) - if err != nil { - return - } - - hasPlainChild := false - for _, header := range multipartHeaders { - mediaType, _, _ := mime.ParseMediaType(header.Get("Content-Type")) - hasPlainChild = (mediaType == "text/plain") - } - if len(multiparts) != 2 { - sc.verified = newSignatureNotSigned() - // Invalid multipart/signed format just pass along - if _, err = ioutil.ReadAll(rawBody); err != nil { - return errors.Wrap(err, "gopenpgp: error in reading raw message body") - } - - for i, p := range multiparts { - if err = sc.target.Accept(p, multipartHeaders[i], hasPlainChild, true, true); err != nil { - return - } - } - return - } - - // actual multipart/signed format - err = sc.target.Accept(multiparts[0], multipartHeaders[0], hasPlainChild, true, true) - if err != nil { - return errors.Wrap(err, "gopenpgp: error in parsing body") - } - - partData, err := ioutil.ReadAll(multiparts[1]) - if err != nil { - return errors.Wrap(err, "gopenpgp: error in ready part data") - } - - decodedPart := gomime.DecodeContentEncoding( - bytes.NewReader(partData), - multipartHeaders[1].Get("Content-Transfer-Encoding")) - - buffer, err := ioutil.ReadAll(decodedPart) - if err != nil { - return errors.Wrap(err, "gopenpgp: error in reading decoded data") - } - mediaType, _, _ := mime.ParseMediaType(header.Get("Content-Type")) - buffer, err = gomime.DecodeCharset(buffer, mediaType, params) - if err != nil { - return errors.Wrap(err, "gopenpgp: error in decoding charset") - } - sc.signature = string(buffer) - str, _ := ioutil.ReadAll(rawBody) - canonicalizedBody := internal.Canonicalize(internal.TrimEachLine(string(str))) - rawBody = bytes.NewReader([]byte(canonicalizedBody)) - if sc.keyring != nil { - _, err = openpgp.CheckArmoredDetachedSignature(sc.keyring, rawBody, bytes.NewReader(buffer), sc.config) - - switch { - case err == nil: - sc.verified = nil - case errors.Is(err, pgpErrors.ErrUnknownIssuer): - sc.verified = newSignatureNoVerifier() - default: - sc.verified = newSignatureFailed(err) - } - } else { - sc.verified = newSignatureNoVerifier() - } - - return nil -} - -// GetSignature collected by Accept. -func (sc SignatureCollector) GetSignature() string { - return sc.signature -} diff --git a/crypto/signature_test.go b/crypto/signature_test.go index de32e1f9..1bfd6936 100644 --- a/crypto/signature_test.go +++ b/crypto/signature_test.go @@ -2,30 +2,26 @@ package crypto import ( "bytes" - "crypto" - "errors" + "encoding/hex" "io" - "io/ioutil" + "os" "regexp" "testing" - "time" - "github.com/ProtonMail/go-crypto/openpgp" "github.com/ProtonMail/go-crypto/openpgp/packet" + "github.com/ProtonMail/gopenpgp/v3/armor" + "github.com/ProtonMail/gopenpgp/v3/constants" + "github.com/ProtonMail/gopenpgp/v3/internal" + "github.com/pkg/errors" "github.com/stretchr/testify/assert" - - "github.com/ProtonMail/gopenpgp/v2/constants" ) -const testMessage = "Hello world!" - const signedPlainText = "Signed message\n" -var textSignature, binSignature *PGPSignature -var message *PlainMessage +var textSignature, binSignature []byte var signatureTest = regexp.MustCompile("(?s)^-----BEGIN PGP SIGNATURE-----.*-----END PGP SIGNATURE-----$") -func getSignatureType(sig *PGPSignature) (packet.SignatureType, error) { +func getSignatureType(sig []byte) (packet.SignatureType, error) { sigPacket, err := getSignaturePacket(sig) if err != nil { return 0, err @@ -33,28 +29,40 @@ func getSignatureType(sig *PGPSignature) (packet.SignatureType, error) { return sigPacket.SigType, nil } -func getSignaturePacket(sig *PGPSignature) (*packet.Signature, error) { - p, err := packet.Read(bytes.NewReader(sig.Data)) - if err != nil { - return nil, err - } - sigPacket, ok := p.(*packet.Signature) - if !ok { - return nil, errors.New("") - } - return sigPacket, nil +func testSignerText() PGPSign { + signer, _ := testPGP.Sign(). + SigningKeys(keyRingTestPrivate). + Utf8(). + Detached(). + New() + return signer +} + +func testSigner() PGPSign { + signer, _ := testPGP.Sign(). + SigningKeys(keyRingTestPrivate). + Detached(). + New() + return signer +} + +func testVerifier() PGPVerify { + verifier, _ := testPGP.Verify(). + VerificationKeys(keyRingTestPublic). + VerifyTime(testTime). + New() + return verifier } func TestSignTextDetached(t *testing.T) { var err error - message = NewPlainMessageFromString(signedPlainText) - textSignature, err = keyRingTestPrivate.SignDetached(message) + textSignature, err = testSignerText().Sign([]byte(signedPlainText), Bytes) if err != nil { t.Fatal("Cannot generate signature:", err) } - armoredSignature, err := textSignature.GetArmored() + armoredSignature, err := armor.ArmorPGPSignatureBinary(textSignature) if err != nil { t.Fatal("Cannot armor signature:", err) } @@ -69,13 +77,33 @@ func TestSignTextDetached(t *testing.T) { t.Fatal("Signature type was not text") } - assert.Regexp(t, signatureTest, armoredSignature) + assert.Regexp(t, signatureTest, string(armoredSignature)) + + verificationError, err := testVerifier().VerifyDetached([]byte(signedPlainText), textSignature, Bytes) + if err != nil { + t.Fatal(err) + } + if err = verificationError.SignatureError(); err != nil { + t.Fatal("Cannot verify plaintext signature:", err) + } + + fakeMessage := []byte("wrong text") + verificationError, err = testVerifier().VerifyDetached(fakeMessage, textSignature, Bytes) + if err != nil { + t.Fatal(err) + } + + checkVerificationError(t, verificationError.SignatureError(), constants.SIGNATURE_FAILED) } -func TestVerifyTextDetachedSig(t *testing.T) { - verificationError := keyRingTestPublic.VerifyDetached(message, textSignature, testTime) - if verificationError != nil { - t.Fatal("Cannot verify plaintext signature:", verificationError) +func TestSignNonUtf8Text(t *testing.T) { + var err error + + var nonUft8, _ = hex.DecodeString("fc80808080af") + + textSignature, err = testSignerText().Sign(nonUft8, Bytes) + if !errors.Is(err, internal.ErrIncorrectUtf8) { + t.Fatal("Expected not valid utf8 error") } } @@ -86,34 +114,22 @@ func checkVerificationError(t *testing.T, err error, expectedStatus int) { castedErr := &SignatureVerificationError{} isType := errors.As(err, castedErr) if !isType { - t.Fatalf("Error was not a verification errror: %v", err) + t.Fatalf("Error was not a verification error: %v", err) } if castedErr.Status != expectedStatus { t.Fatalf("Expected status to be %d got %d", expectedStatus, castedErr.Status) } } -func TestVerifyTextDetachedSigWrong(t *testing.T) { - fakeMessage := NewPlainMessageFromString("wrong text") - verificationError := keyRingTestPublic.VerifyDetached(fakeMessage, textSignature, testTime) - - checkVerificationError(t, verificationError, constants.SIGNATURE_FAILED) - - err := &SignatureVerificationError{} - _ = errors.As(verificationError, err) - assert.Exactly(t, constants.SIGNATURE_FAILED, err.Status) -} - func TestSignBinDetached(t *testing.T) { var err error - message = NewPlainMessage([]byte(signedPlainText)) - binSignature, err = keyRingTestPrivate.SignDetached(message) + binSignature, err = testSigner().Sign([]byte(signedPlainText), Bytes) if err != nil { t.Fatal("Cannot generate signature:", err) } - armoredSignature, err := binSignature.GetArmored() + armoredSignature, err := armor.ArmorPGPSignatureBinary(binSignature) if err != nil { t.Fatal("Cannot armor signature:", err) } @@ -128,74 +144,63 @@ func TestSignBinDetached(t *testing.T) { t.Fatal("Signature type was not binary") } - assert.Regexp(t, signatureTest, armoredSignature) -} + assert.Regexp(t, signatureTest, string(armoredSignature)) -func TestVerifyBinDetachedSig(t *testing.T) { - verificationError := keyRingTestPublic.VerifyDetached(message, binSignature, testTime) - if verificationError != nil { - t.Fatal("Cannot verify binary signature:", verificationError) + verificationError, err := testVerifier().VerifyDetached([]byte(signedPlainText), binSignature, Bytes) + if err != nil { + t.Fatal(err) + } + if err = verificationError.SignatureError(); err != nil { + t.Fatal("Cannot verify binary signature:", err) } } func Test_KeyRing_GetVerifiedSignatureTimestampSuccess(t *testing.T) { - message := NewPlainMessageFromString(testMessage) - var time int64 = 1600000000 - pgp.latestServerTime = time - defer func() { - pgp.latestServerTime = testTime - }() - signature, err := keyRingTestPrivate.SignDetached(message) + message := []byte(testMessage) + var timeLocal int64 = 1600000000 + signer, _ := testPGP.Sign(). + SigningKeys(keyRingTestPrivate). + SignTime(timeLocal). + Detached(). + New() + signature, err := signer.Sign(message, Bytes) if err != nil { t.Errorf("Got an error while generating the signature: %v", err) } - actualTime, err := keyRingTestPublic.GetVerifiedSignatureTimestamp(message, signature, 0) + verifier, _ := testPGP.Verify(). + VerificationKeys(keyRingTestPublic). + VerifyTime(timeLocal). + New() + verificationResult, err := verifier.VerifyDetached(message, signature, Bytes) if err != nil { - t.Errorf("Got an error while parsing the signature creation time: %v", err) - } - if time != actualTime { - t.Errorf("Expected creation time to be %d, got %d", time, actualTime) - } -} - -func Test_KeyRing_GetVerifiedSignatureTimestampWithContext(t *testing.T) { - message := NewPlainMessageFromString(testMessage) - var time int64 = 1600000000 - pgp.latestServerTime = time - defer func() { - pgp.latestServerTime = testTime - }() - var testContext = "test-context" - signature, err := keyRingTestPrivate.SignDetachedWithContext(message, NewSigningContext(testContext, true)) - if err != nil { - t.Errorf("Got an error while generating the signature: %v", err) + t.Fatal(err) } - actualTime, err := keyRingTestPublic.GetVerifiedSignatureTimestampWithContext(message, signature, 0, NewVerificationContext(testContext, true, 0)) + actualTime := verificationResult.SignatureCreationTime() if err != nil { t.Errorf("Got an error while parsing the signature creation time: %v", err) } - if time != actualTime { - t.Errorf("Expected creation time to be %d, got %d", time, actualTime) + if timeLocal != actualTime { + t.Errorf("Expected creation time to be %d, got %d", timeLocal, actualTime) } } func Test_KeyRing_GetVerifiedSignatureWithTwoKeysTimestampSuccess(t *testing.T) { - publicKey1Armored, err := ioutil.ReadFile("testdata/signature/publicKey1") + publicKey1Armored, err := os.ReadFile("testdata/signature/publicKey1") if err != nil { t.Errorf("Couldn't read the public key file: %v", err) } publicKey1 := parseKey(t, string(publicKey1Armored)) - publicKey2Armored, err := ioutil.ReadFile("testdata/signature/publicKey2") + publicKey2Armored, err := os.ReadFile("testdata/signature/publicKey2") if err != nil { t.Errorf("Couldn't read the public key file: %v", err) } publicKey2 := parseKey(t, string(publicKey2Armored)) - message := NewPlainMessageFromString("hello world") - signatureArmored, err := ioutil.ReadFile("testdata/signature/detachedSigSignedTwice") + message := []byte("hello world") + signatureArmored, err := os.ReadFile("testdata/signature/detachedSigSignedTwice") if err != nil { t.Errorf("Couldn't read the signature file: %v", err) } - signature, err := NewPGPSignatureFromArmored(string(signatureArmored)) + signature, err := armor.UnarmorBytes(signatureArmored) if err != nil { t.Errorf("Got an error while parsing the signature: %v", err) } @@ -209,15 +214,27 @@ func Test_KeyRing_GetVerifiedSignatureWithTwoKeysTimestampSuccess(t *testing.T) if err != nil { t.Errorf("Got an error while adding key 2 to the key ring: %v", err) } - actualTime, err := keyRing.GetVerifiedSignatureTimestamp(message, signature, 0) + + verifier, _ := testPGP.Verify(). + VerificationKeys(keyRing). + DisableVerifyTimeCheck(). + New() + + verificationResult, err := verifier.VerifyDetached(message, signature, Bytes) + if err != nil { + t.Fatal(err) + } + actualTime := verificationResult.SignatureCreationTime() + otherTime := verificationResult.Signatures[1].Signature.CreationTime.Unix() if err != nil { t.Errorf("Got an error while parsing the signature creation time: %v", err) } - if time1 != actualTime { - t.Errorf("Expected creation time to be %d, got %d", time1, actualTime) + + if time2 != otherTime { + t.Errorf("Expected creation time to be %d, got %d", otherTime, time2) } - if time2 == actualTime { - t.Errorf("Expected creation time to be different from %d", time2) + if time1 != actualTime { + t.Errorf("Expected creation time to be %d, got %d", actualTime, time1) } } @@ -230,8 +247,8 @@ func parseKey(t *testing.T, keyArmored string) *Key { return key } -func getTimestampOfIssuer(signature *PGPSignature, keyID uint64) int64 { - packets := packet.NewReader(bytes.NewReader(signature.Data)) +func getTimestampOfIssuer(signature []byte, keyID uint64) int64 { + packets := packet.NewReader(bytes.NewReader(signature)) var err error var p packet.Packet for { @@ -259,19 +276,27 @@ func getTimestampOfIssuer(signature *PGPSignature, keyID uint64) int64 { } func Test_KeyRing_GetVerifiedSignatureTimestampError(t *testing.T) { - message := NewPlainMessageFromString(testMessage) - var time int64 = 1600000000 - pgp.latestServerTime = time - defer func() { - pgp.latestServerTime = testTime - }() - signature, err := keyRingTestPrivate.SignDetached(message) + message := []byte(testMessage) + var timeLocal int64 = 1600000000 + signer, _ := testPGP.Sign(). + SignTime(timeLocal). + SigningKeys(keyRingTestPrivate). + Detached(). + New() + signature, err := signer.Sign(message, Bytes) if err != nil { t.Errorf("Got an error while generating the signature: %v", err) } - messageCorrupted := NewPlainMessageFromString("Ciao world!") - _, err = keyRingTestPublic.GetVerifiedSignatureTimestamp(messageCorrupted, signature, 0) - if err == nil { + messageCorrupted := []byte("Ciao world!") + verifier, _ := testPGP.Verify(). + VerificationKeys(keyRingTestPublic). + VerifyTime(timeLocal). + New() + verificationResult, err := verifier.VerifyDetached(messageCorrupted, signature, Bytes) + if err != nil { + t.Fatal(err) + } + if verificationResult.SignatureError() == nil { t.Errorf("Expected an error while parsing the creation time of a wrong signature, got nil") } } @@ -283,16 +308,18 @@ func Test_SignDetachedWithNonCriticalContext(t *testing.T) { "test-context", false, ) + signer, _ := testPGP.Sign(). + SigningKeys(keyRingTestPrivate). + SigningContext(context). + Detached(). + New() // when - signature, err := keyRingTestPrivate.SignDetachedWithContext( - NewPlainMessage([]byte(testMessage)), - context, - ) + signature, err := signer.Sign([]byte(testMessage), Bytes) // then if err != nil { t.Fatal(err) } - p, err := packet.Read(bytes.NewReader(signature.Data)) + p, err := packet.Read(bytes.NewReader(signature)) if err != nil { t.Fatal(err) } @@ -301,7 +328,7 @@ func Test_SignDetachedWithNonCriticalContext(t *testing.T) { t.Fatal("Packet was not a signature") } notations := sig.Notations - if len(notations) != 1 { + if len(notations) != 2 { t.Fatal("Wrong number of notations") } notation := notations[0] @@ -326,16 +353,18 @@ func Test_SignDetachedWithCriticalContext(t *testing.T) { "test-context", true, ) + signer, _ := testPGP.Sign(). + SigningKeys(keyRingTestPrivate). + SigningContext(context). + Detached(). + New() // when - signature, err := keyRingTestPrivate.SignDetachedWithContext( - NewPlainMessage([]byte(testMessage)), - context, - ) + signature, err := signer.Sign([]byte(testMessage), Bytes) // then if err != nil { t.Fatal(err) } - p, err := packet.Read(bytes.NewReader(signature.Data)) + p, err := packet.Read(bytes.NewReader(signature)) if err != nil { t.Fatal(err) } @@ -344,7 +373,7 @@ func Test_SignDetachedWithCriticalContext(t *testing.T) { t.Fatal("Packet was not a signature") } notations := sig.Notations - if len(notations) != 1 { + if len(notations) != 2 { t.Fatal("Wrong number of notations") } notation := notations[0] @@ -362,59 +391,65 @@ func Test_SignDetachedWithCriticalContext(t *testing.T) { } } -func Test_VerifyDetachedWithUnknownCriticalContext(t *testing.T) { +func Test_VerifyWithUnknownCriticalContext(t *testing.T) { // given - signatureArmored, err := ioutil.ReadFile("testdata/signature/critical_context_detached_sig") + signatureArmored, err := os.ReadFile("testdata/signature/critical_context_detached_sig") if err != nil { t.Fatal(err) } - sig, err := NewPGPSignatureFromArmored(string(signatureArmored)) + sig, err := armor.UnarmorBytes(signatureArmored) if err != nil { t.Fatal(err) } // when - err = keyRingTestPublic.VerifyDetached( - NewPlainMessage([]byte(testMessage)), - sig, - 0, - ) + verifier, _ := testPGP.Verify(). + VerificationKeys(keyRingTestPublic). + DisableVerifyTimeCheck(). + New() + result, err := verifier.VerifyDetached([]byte(testMessage), sig, Bytes) + if err != nil { + t.Fatal(err) + } // then - checkVerificationError(t, err, constants.SIGNATURE_FAILED) + checkVerificationError(t, result.SignatureError(), constants.SIGNATURE_FAILED) } -func Test_VerifyDetachedWithUnKnownNonCriticalContext(t *testing.T) { +func Test_VerifyWithUnKnownNonCriticalContext(t *testing.T) { // given - signatureArmored, err := ioutil.ReadFile("testdata/signature/non_critical_context_detached_sig") + signatureArmored, err := os.ReadFile("testdata/signature/non_critical_context_detached_sig") if err != nil { t.Fatal(err) } - sig, err := NewPGPSignatureFromArmored(string(signatureArmored)) + sig, err := armor.UnarmorBytes(signatureArmored) if err != nil { t.Fatal(err) } // when - err = keyRingTestPublic.VerifyDetached( - NewPlainMessage([]byte(testMessage)), - sig, - 0, - ) - // then + verifier, _ := testPGP.Verify(). + VerificationKeys(keyRingTestPublic). + DisableVerifyTimeCheck(). + New() + result, err := verifier.VerifyDetached([]byte(testMessage), sig, Bytes) if err != nil { + t.Fatal(err) + } + // then + if err = result.SignatureError(); err != nil { t.Fatalf("Expected no verification error, got %v", err) } } -func Test_VerifyDetachedWithKnownCriticalContext(t *testing.T) { +func Test_VerifyWithKnownCriticalContext(t *testing.T) { // given - signatureArmored, err := ioutil.ReadFile("testdata/signature/critical_context_detached_sig") + signatureArmored, err := os.ReadFile("testdata/signature/critical_context_detached_sig") if err != nil { t.Fatal(err) } - sig, err := NewPGPSignatureFromArmored(string(signatureArmored)) + sig, err := armor.UnarmorBytes(signatureArmored) if err != nil { t.Fatal(err) } @@ -424,26 +459,29 @@ func Test_VerifyDetachedWithKnownCriticalContext(t *testing.T) { 0, ) // when - err = keyRingTestPublic.VerifyDetachedWithContext( - NewPlainMessage([]byte(testMessage)), - sig, - 0, - verificationContext, - ) - // then + verifier, _ := testPGP.Verify(). + VerificationKeys(keyRingTestPublic). + VerificationContext(verificationContext). + DisableVerifyTimeCheck(). + New() + result, err := verifier.VerifyDetached([]byte(testMessage), sig, Bytes) if err != nil { + t.Fatal(err) + } + // then + if err = result.SignatureError(); err != nil { t.Fatalf("Expected no verification error, got %v", err) } } -func Test_VerifyDetachedWithWrongContext(t *testing.T) { +func Test_VerifyWithWrongContext(t *testing.T) { // given - signatureArmored, err := ioutil.ReadFile("testdata/signature/critical_context_detached_sig") + signatureArmored, err := os.ReadFile("testdata/signature/critical_context_detached_sig") if err != nil { t.Fatal(err) } - sig, err := NewPGPSignatureFromArmored(string(signatureArmored)) + sig, err := armor.UnarmorBytes(signatureArmored) if err != nil { t.Fatal(err) } @@ -453,24 +491,27 @@ func Test_VerifyDetachedWithWrongContext(t *testing.T) { 0, ) // when - err = keyRingTestPublic.VerifyDetachedWithContext( - NewPlainMessage([]byte(testMessage)), - sig, - 0, - verificationContext, - ) + verifier, _ := testPGP.Verify(). + VerificationKeys(keyRingTestPublic). + VerificationContext(verificationContext). + DisableVerifyTimeCheck(). + New() + result, err := verifier.VerifyDetached([]byte(testMessage), sig, Bytes) + if err != nil { + t.Fatal(err) + } // then - checkVerificationError(t, err, constants.SIGNATURE_BAD_CONTEXT) + checkVerificationError(t, result.SignatureError(), constants.SIGNATURE_BAD_CONTEXT) } -func Test_VerifyDetachedWithMissingNonRequiredContext(t *testing.T) { +func Test_VerifyWithMissingNonRequiredContext(t *testing.T) { // given - signatureArmored, err := ioutil.ReadFile("testdata/signature/no_context_detached_sig") + signatureArmored, err := os.ReadFile("testdata/signature/no_context_detached_sig") if err != nil { t.Fatal(err) } - sig, err := NewPGPSignatureFromArmored(string(signatureArmored)) + sig, err := armor.UnarmorBytes(signatureArmored) if err != nil { t.Fatal(err) } @@ -480,26 +521,29 @@ func Test_VerifyDetachedWithMissingNonRequiredContext(t *testing.T) { 0, ) // when - err = keyRingTestPublic.VerifyDetachedWithContext( - NewPlainMessage([]byte(testMessage)), - sig, - 0, - verificationContext, - ) - // then + verifier, _ := testPGP.Verify(). + VerificationKeys(keyRingTestPublic). + VerificationContext(verificationContext). + DisableVerifyTimeCheck(). + New() + result, err := verifier.VerifyDetached([]byte(testMessage), sig, Bytes) if err != nil { + t.Fatal(err) + } + // then + if err = result.SignatureError(); err != nil { t.Fatalf("Expected no verification error, got %v", err) } } -func Test_VerifyDetachedWithMissingRequiredContext(t *testing.T) { +func Test_VerifyWithMissingRequiredContext(t *testing.T) { // given - signatureArmored, err := ioutil.ReadFile("testdata/signature/no_context_detached_sig") + signatureArmored, err := os.ReadFile("testdata/signature/no_context_detached_sig") if err != nil { t.Fatal(err) } - sig, err := NewPGPSignatureFromArmored(string(signatureArmored)) + sig, err := armor.UnarmorBytes(signatureArmored) if err != nil { t.Fatal(err) } @@ -509,27 +553,30 @@ func Test_VerifyDetachedWithMissingRequiredContext(t *testing.T) { 0, ) // when - err = keyRingTestPublic.VerifyDetachedWithContext( - NewPlainMessage([]byte(testMessage)), - sig, - 0, - verificationContext, - ) + verifier, _ := testPGP.Verify(). + VerificationKeys(keyRingTestPublic). + VerificationContext(verificationContext). + DisableVerifyTimeCheck(). + New() + result, err := verifier.VerifyDetached([]byte(testMessage), sig, Bytes) + if err != nil { + t.Fatal(err) + } // then - checkVerificationError(t, err, constants.SIGNATURE_BAD_CONTEXT) + checkVerificationError(t, result.SignatureError(), constants.SIGNATURE_BAD_CONTEXT) } -func Test_VerifyDetachedWithMissingRequiredContextBeforeCutoff(t *testing.T) { +func Test_VerifyWithMissingRequiredContextBeforeCutoff(t *testing.T) { // given - signatureArmored, err := ioutil.ReadFile("testdata/signature/no_context_detached_sig") + signatureArmored, err := os.ReadFile("testdata/signature/no_context_detached_sig") if err != nil { t.Fatal(err) } - sig, err := NewPGPSignatureFromArmored(string(signatureArmored)) + sig, err := armor.UnarmorBytes(signatureArmored) if err != nil { t.Fatal(err) } - p, err := packet.Read(bytes.NewReader(sig.Data)) + p, err := packet.Read(bytes.NewReader(sig)) if err != nil { t.Fatal(err) } @@ -543,29 +590,32 @@ func Test_VerifyDetachedWithMissingRequiredContextBeforeCutoff(t *testing.T) { sigPacket.CreationTime.Unix()+10000, ) // when - err = keyRingTestPublic.VerifyDetachedWithContext( - NewPlainMessage([]byte(testMessage)), - sig, - 0, - verificationContext, - ) - // then + verifier, _ := testPGP.Verify(). + VerificationKeys(keyRingTestPublic). + VerificationContext(verificationContext). + DisableVerifyTimeCheck(). + New() + result, err := verifier.VerifyDetached([]byte(testMessage), sig, Bytes) if err != nil { + t.Fatal(err) + } + // then + if err = result.SignatureError(); err != nil { t.Fatalf("Expected no verification error, got %v", err) } } -func Test_VerifyDetachedWithMissingRequiredContextAfterCutoff(t *testing.T) { +func Test_VerifyWithMissingRequiredContextAfterCutoff(t *testing.T) { // given - signatureArmored, err := ioutil.ReadFile("testdata/signature/no_context_detached_sig") + signatureArmored, err := os.ReadFile("testdata/signature/no_context_detached_sig") if err != nil { t.Fatal(err) } - sig, err := NewPGPSignatureFromArmored(string(signatureArmored)) + sig, err := armor.UnarmorBytes(signatureArmored) if err != nil { t.Fatal(err) } - p, err := packet.Read(bytes.NewReader(sig.Data)) + p, err := packet.Read(bytes.NewReader(sig)) if err != nil { t.Fatal(err) } @@ -579,23 +629,26 @@ func Test_VerifyDetachedWithMissingRequiredContextAfterCutoff(t *testing.T) { sigPacket.CreationTime.Unix()-10000, ) // when - err = keyRingTestPublic.VerifyDetachedWithContext( - NewPlainMessage([]byte(testMessage)), - sig, - 0, - verificationContext, - ) + verifier, _ := testPGP.Verify(). + VerificationKeys(keyRingTestPublic). + VerificationContext(verificationContext). + DisableVerifyTimeCheck(). + New() + result, err := verifier.VerifyDetached([]byte(testMessage), sig, Bytes) + if err != nil { + t.Fatal(err) + } // then - checkVerificationError(t, err, constants.SIGNATURE_BAD_CONTEXT) + checkVerificationError(t, result.SignatureError(), constants.SIGNATURE_BAD_CONTEXT) } -func Test_VerifyDetachedWithDoubleContext(t *testing.T) { +func Test_VerifyWithDoubleContext(t *testing.T) { // given - signatureArmored, err := ioutil.ReadFile("testdata/signature/double_critical_context_detached_sig") + signatureArmored, err := os.ReadFile("testdata/signature/double_critical_context_detached_sig") if err != nil { t.Fatal(err) } - sig, err := NewPGPSignatureFromArmored(string(signatureArmored)) + sig, err := armor.UnarmorBytes(signatureArmored) if err != nil { t.Fatal(err) } @@ -605,61 +658,15 @@ func Test_VerifyDetachedWithDoubleContext(t *testing.T) { 0, ) // when - err = keyRingTestPublic.VerifyDetachedWithContext( - NewPlainMessage([]byte(testMessage)), - sig, - 0, - verificationContext, - ) - // then - checkVerificationError(t, err, constants.SIGNATURE_BAD_CONTEXT) -} - -func Test_verifySignaturExpire(t *testing.T) { - defer func(t int64) { pgp.latestServerTime = t }(pgp.latestServerTime) - pgp.latestServerTime = 0 - - const lifetime = uint32(time.Hour / time.Second) - - cfg := &packet.Config{ - Algorithm: packet.PubKeyAlgoEdDSA, - DefaultHash: crypto.SHA256, - DefaultCipher: packet.CipherAES256, - DefaultCompressionAlgo: packet.CompressionZLIB, - KeyLifetimeSecs: lifetime, - SigLifetimeSecs: lifetime, - } - - entity, err := openpgp.NewEntity("John Smith", "Linux", "john.smith@example.com", cfg) - if err != nil { - t.Fatal(err) - } - - key, err := NewKeyFromEntity(entity) - if err != nil { - t.Fatal(err) - } - - keyRing, err := NewKeyRing(key) - if err != nil { - t.Fatal(err) - } - - data := []byte("Hello, World!") - message := NewPlainMessage(data) - - signature, err := keyRing.SignDetached(message) - if err != nil { - t.Fatalf("%#+v", err) - } - - sig := NewPGPSignature(signature.GetBinary()) - - // packet.PublicKey.KeyExpired will return false here because PublicKey CreationTime has - // nanosecond precision, while pgpcrypto.GetUnixTime() has only second precision. - // Adjust the check time to be in the future to ensure that the key is not expired. - err = keyRing.VerifyDetached(message, sig, GetUnixTime()+1) + verifier, _ := testPGP.Verify(). + VerificationKeys(keyRingTestPublic). + VerificationContext(verificationContext). + DisableVerifyTimeCheck(). + New() + result, err := verifier.VerifyDetached([]byte(testMessage), sig, Bytes) if err != nil { t.Fatal(err) } + // then + checkVerificationError(t, result.SignatureError(), constants.SIGNATURE_BAD_CONTEXT) } diff --git a/crypto/time.go b/crypto/time.go index b208d874..d804b88b 100644 --- a/crypto/time.go +++ b/crypto/time.go @@ -4,66 +4,19 @@ import ( "time" ) -// UpdateTime updates cached time. -func UpdateTime(newTime int64) { - pgp.lock.Lock() - defer pgp.lock.Unlock() +// Clock is a function that returns a timestamp. +type Clock func() time.Time - if newTime > pgp.latestServerTime { - pgp.latestServerTime = newTime +// NewConstantClock returns a Clock, which always returns unixTime. +func NewConstantClock(unixTime int64) Clock { + return func() time.Time { + return time.Unix(unixTime, 0) } } -// SetKeyGenerationOffset updates the offset when generating keys. -func SetKeyGenerationOffset(offset int64) { - pgp.lock.Lock() - defer pgp.lock.Unlock() - - pgp.generationOffset = offset -} - -// GetUnixTime gets latest cached time. -func GetUnixTime() int64 { - return getNow().Unix() -} - -// GetTime gets latest cached time. -func GetTime() time.Time { - return getNow() -} - -// ----- INTERNAL FUNCTIONS ----- - -// getNow returns the latest server time. -func getNow() time.Time { - pgp.lock.RLock() - defer pgp.lock.RUnlock() - - if pgp.latestServerTime == 0 { - return time.Now() +// ZeroClock returns a Clock, which always returns the zero time.Time. +func ZeroClock() Clock { + return func() time.Time { + return time.Time{} } - - return time.Unix(pgp.latestServerTime, 0) -} - -// getTimeGenerator Returns a time generator function. -func getTimeGenerator() func() time.Time { - return getNow -} - -// getNowKeyGenerationOffset returns the current time with the key generation offset. -func getNowKeyGenerationOffset() time.Time { - pgp.lock.RLock() - defer pgp.lock.RUnlock() - - if pgp.latestServerTime == 0 { - return time.Unix(time.Now().Unix()+pgp.generationOffset, 0) - } - - return time.Unix(pgp.latestServerTime+pgp.generationOffset, 0) -} - -// getKeyGenerationTimeGenerator Returns a time generator function with the key generation offset. -func getKeyGenerationTimeGenerator() func() time.Time { - return getNowKeyGenerationOffset } diff --git a/crypto/time_test.go b/crypto/time_test.go deleted file mode 100644 index 50d3ae35..00000000 --- a/crypto/time_test.go +++ /dev/null @@ -1,17 +0,0 @@ -package crypto - -import ( - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -func TestTime(t *testing.T) { - UpdateTime(1571072494) - time.Sleep(1 * time.Second) - now := GetUnixTime() - - assert.Exactly(t, int64(1571072494), now) // Use latest server time - UpdateTime(testTime) -} diff --git a/crypto/verify.go b/crypto/verify.go new file mode 100644 index 00000000..b430210a --- /dev/null +++ b/crypto/verify.go @@ -0,0 +1,35 @@ +package crypto + +// PGPVerify is an interface for verifying detached signatures with GopenPGP. +type PGPVerify interface { + // VerifyingReader wraps a reader with a signature verify reader. + // Once all data is read from the returned verify reader, the signature can be verified + // with (VerifyDataReader).VerifySignature(). + // Note that an error is only returned if it is not a signature error. + // The encoding indicates if the input signature message should be unarmored or not, + // i.e., Bytes/Armor/Auto where Auto tries to detect it automatically. + // If detachedData is nil, signatureMessage is treated as an inline signature message. + // Thus, it is expected that signatureMessage contains the data to be verified. + // If detachedData is not nil, signatureMessage must contain a detached signature, + // which is verified against the detachedData. + VerifyingReader(detachedData, signatureMessage Reader, encoding int8) (*VerifyDataReader, error) + // VerifyDetached verifies a detached signature pgp message + // and returns a VerifyResult. The VerifyResult can be checked for failure + // and allows access to information about the signatures. + // Note that an error is only returned if it is not a signature error. + // The encoding indicates if the input signature message should be unarmored or not, + // i.e., Bytes/Armor/Auto where Auto tries to detect it automatically. + VerifyDetached(data []byte, signature []byte, encoding int8) (*VerifyResult, error) + // VerifyInline verifies an inline signed pgp message + // and returns a VerifiedDataResult. The VerifiedDataResult can be checked for failure, + // allows access to information about the signatures, and includes the plain message. + // Note that an error is only returned if it is not a signature error. + // The encoding indicates if the input message should be unarmored or not, i.e., Bytes/Armor/Auto + // where Auto tries to detect it automatically. + VerifyInline(message []byte, encoding int8) (*VerifiedDataResult, error) + // VerifyCleartext verifies an armored cleartext message + // and returns a VerifyCleartextResult. The VerifyCleartextResult can be checked for failure + // and allows access the contained message + // Note that an error is only returned if it is not a signature error. + VerifyCleartext(cleartext []byte) (*VerifyCleartextResult, error) +} diff --git a/crypto/verify_handle.go b/crypto/verify_handle.go new file mode 100644 index 00000000..3cc7d9bd --- /dev/null +++ b/crypto/verify_handle.go @@ -0,0 +1,270 @@ +package crypto + +import ( + "bytes" + "io" + + "github.com/ProtonMail/go-crypto/openpgp/armor" + "github.com/ProtonMail/go-crypto/openpgp/clearsign" + "github.com/ProtonMail/go-crypto/openpgp/packet" + openpgp "github.com/ProtonMail/go-crypto/openpgp/v2" + "github.com/ProtonMail/gopenpgp/v3/constants" + "github.com/ProtonMail/gopenpgp/v3/internal" + "github.com/pkg/errors" +) + +type verifyHandle struct { + VerifyKeyRing *KeyRing + VerificationContext *VerificationContext + DisableVerifyTimeCheck bool + DisableStrictMessageParsing bool + DisableAutomaticTextSanitize bool + IsUTF8 bool + clock Clock + profile SignProfile +} + +// --- Default verification handle to build from + +func defaultVerifyHandle(profile SignProfile, clock Clock) *verifyHandle { + return &verifyHandle{ + clock: clock, + profile: profile, + } +} + +// --- Implements VerifyHandle functions + +// VerifyingReader wraps a reader with a signature verify reader. +// Once all data is read from the returned verify reader, the signature can be verified +// with (VerifyDataReader).VerifySignature(). +// Note that an error is only returned if it is not a signature error. +// The encoding indicates if the input signature message should be unarmored or not, +// i.e., Bytes/Armor/Auto where Auto tries to detect it automatically. +// If detachedData is nil, signatureMessage is treated as an inline signature message. +// Thus, it is expected that signatureMessage contains the data to be verified. +// If detachedData is not nil, signatureMessage must contain a detached signature, +// which is verified against the detachedData. +func (vh *verifyHandle) VerifyingReader(detachedData, signatureMessage Reader, encoding int8) (reader *VerifyDataReader, err error) { + var armored bool + signatureMessage, armored = unarmorInput(encoding, signatureMessage) + if armored { + // Wrap with decode armor reader. + armoredBlock, err := armor.Decode(signatureMessage) + if err != nil { + return nil, errors.Wrap(err, "gopenpgp: unarmor failed") + } + signatureMessage = armoredBlock.Body + } + if detachedData != nil { + if vh.IsUTF8 { + detachedData = openpgp.NewCanonicalTextReader(detachedData) + } + reader, err = vh.verifyingDetachedReader(detachedData, signatureMessage) + } else { + reader, err = vh.verifyingReader(signatureMessage) + if err == nil && vh.IsUTF8 { + reader.internalReader = internal.NewSanitizeReader(reader.internalReader) + } + } + return +} + +// VerifyDetached verifies a detached signature pgp message +// and returns a VerifyResult. The VerifyResult can be checked for failure +// and allows access to information about the signatures. +// Note that an error is only returned if it is not a signature error. +// The encoding indicates if the input signature message should be unarmored or not, +// i.e., Bytes/Armor/Auto where Auto tries to detect it automatically. +func (vh *verifyHandle) VerifyDetached(data, signature []byte, encoding int8) (verifyResult *VerifyResult, err error) { + signatureMessageReader := bytes.NewReader(signature) + detachedDataReader := bytes.NewReader(data) + ptReader, err := vh.VerifyingReader(detachedDataReader, signatureMessageReader, encoding) + if err != nil { + return nil, errors.Wrap(err, "gopenpgp: verifying signature failed") + } + _, err = io.Copy(io.Discard, ptReader) + if err != nil { + return nil, errors.Wrap(err, "gopenpgp: reading data to verify signature failed") + } + return ptReader.VerifySignature() +} + +// VerifyInline verifies an inline signed pgp message +// and returns a VerifiedDataResult. The VerifiedDataResult can be checked for failure, +// allows access to information about the signatures, and includes the plain message. +// Note that an error is only returned if it is not a signature error. +// The encoding indicates if the input message should be unarmored or not, i.e., Bytes/Armor/Auto +// where Auto tries to detect it automatically. +func (vh *verifyHandle) VerifyInline(message []byte, encoding int8) (verifyDataResult *VerifiedDataResult, err error) { + var ptReader *VerifyDataReader + messageReader := bytes.NewReader(message) + ptReader, err = vh.VerifyingReader(nil, messageReader, encoding) + if err != nil { + return nil, errors.Wrap(err, "gopenpgp: verifying signature failed") + } + data, err := ptReader.ReadAll() + if err != nil { + return nil, errors.Wrap(err, "gopenpgp: reading data to verify signature failed") + } + verifyResult, err := ptReader.VerifySignature() + if err != nil { + return nil, errors.Wrap(err, "gopenpgp: verifying signature failed") + } + verifyDataResult = &VerifiedDataResult{ + data: data, + metadata: ptReader.GetMetadata(), + VerifyResult: *verifyResult, + } + return +} + +// VerifyCleartext verifies an armored cleartext message and returns a VerifyCleartextResult. +// The VerifyCleartextResult can be checked for failure and allows access the contained message. +// Note that an error is only returned if it is not a signature error. +func (vh *verifyHandle) VerifyCleartext(cleartext []byte) (*VerifyCleartextResult, error) { + return vh.verifyCleartext(cleartext) +} + +// --- Private logic functions + +func (vh *verifyHandle) validate() error { + if vh.VerifyKeyRing == nil { + return errors.New("gopenpgp: no verification key provided") + } + return nil +} + +// verifyDetachedSignature verifies if a detached signature is valid with the entity list. +func (vh *verifyHandle) verifyDetachedSignature( + origText io.Reader, + signature []byte, +) (result *VerifyResult, err error) { + signatureReader := bytes.NewReader(signature) + ptReader, err := vh.verifyingDetachedReader(origText, signatureReader) + if err != nil { + return nil, errors.Wrap(err, "gopenpgp: verify signature failed") + } + _, err = io.Copy(io.Discard, ptReader) + if err != nil { + return nil, errors.Wrap(err, "gopenpgp: reading all data from plaintext reader failed") + } + return ptReader.VerifySignature() +} + +func (vh *verifyHandle) verifyingReader( + signatureMessage io.Reader, +) (reader *VerifyDataReader, err error) { + checkPacketSequence := !vh.DisableStrictMessageParsing + config := vh.profile.SignConfig() + config.CheckPacketSequence = &checkPacketSequence + verifyTime := vh.clock().Unix() + config.Time = NewConstantClock(verifyTime) + if vh.VerificationContext != nil { + config.KnownNotations = map[string]bool{constants.SignatureContextName: true} + } + md, err := openpgp.ReadMessage( + signatureMessage, + vh.VerifyKeyRing.getEntities(), + nil, + config, + ) + if err != nil { + return nil, errors.Wrap(err, "gopenpgp: initialize signature reader failed") + } + return &VerifyDataReader{ + md, + md.UnverifiedBody, + vh.VerifyKeyRing, + verifyTime, + vh.DisableVerifyTimeCheck, + false, + vh.VerificationContext, + }, nil +} + +func (vh *verifyHandle) verifyingDetachedReader( + data Reader, + signature Reader, +) (*VerifyDataReader, error) { + return verifyingDetachedReader( + data, + signature, + vh.VerifyKeyRing, + vh.VerificationContext, + vh.DisableVerifyTimeCheck, + vh.DisableAutomaticTextSanitize, + vh.profile.SignConfig(), + vh.clock, + ) +} + +func (vh *verifyHandle) verifyCleartext(cleartext []byte) (*VerifyCleartextResult, error) { + block, rest := clearsign.Decode(cleartext) + if block == nil { + return nil, errors.New("gopenpgp: not able to parse cleartext message") + } + if len(bytes.TrimSpace(rest)) > 0 { + return nil, errors.New("gopenpgp: cleartext message has trailing text") + } + signature, err := io.ReadAll(block.ArmoredSignature.Body) + if err != nil { + return nil, errors.Wrap(err, "gopenpgp: signature not parsable in cleartext") + } + reader := bytes.NewReader(block.Bytes) + result, err := vh.verifyDetachedSignature( + reader, + signature, + ) + if err != nil { + return nil, errors.Wrap(err, "gopenpgp: cleartext verify failed with non-signature error") + } + return &VerifyCleartextResult{ + VerifyResult: *result, + cleartext: block.Plaintext[:len(block.Plaintext)-1], + }, nil +} + +func verifyingDetachedReader( + data Reader, + signature Reader, + verifyKeyRing *KeyRing, + verificationContext *VerificationContext, + disableVerifyTimeCheck bool, + disableAutomaticTextSanitize bool, + config *packet.Config, + clock Clock, +) (*VerifyDataReader, error) { + if config == nil { + config = &packet.Config{} + } + verifyTime := clock().Unix() + config.Time = NewConstantClock(verifyTime) + if verificationContext != nil { + config.KnownNotations = map[string]bool{constants.SignatureContextName: true} + } + md, err := openpgp.VerifyDetachedSignatureReader( + verifyKeyRing.getEntities(), + data, + signature, + config, + ) + if err != nil { + return nil, errors.Wrap(err, "gopenpgp: verify signature reader failed") + } + internalReader := md.UnverifiedBody + if len(md.SignatureCandidates) > 0 && + !disableAutomaticTextSanitize && + md.SignatureCandidates[0].SigType == packet.SigTypeText { + internalReader = internal.NewSanitizeReader(internalReader) + } + return &VerifyDataReader{ + md, + internalReader, + verifyKeyRing, + verifyTime, + disableVerifyTimeCheck, + false, + verificationContext, + }, nil +} diff --git a/crypto/verify_handle_builder.go b/crypto/verify_handle_builder.go new file mode 100644 index 00000000..9bef35df --- /dev/null +++ b/crypto/verify_handle_builder.go @@ -0,0 +1,102 @@ +package crypto + +// VerifyHandleBuilder configures a VerifyHandle handle. +type VerifyHandleBuilder struct { + handle *verifyHandle + defaultClock Clock + err error + profile SignProfile +} + +func newVerifyHandleBuilder(profile SignProfile, clock Clock) *VerifyHandleBuilder { + return &VerifyHandleBuilder{ + handle: defaultVerifyHandle(profile, clock), + defaultClock: clock, + profile: profile, + } +} + +// VerificationKeys sets the public keys for verifying the signatures. +func (vhb *VerifyHandleBuilder) VerificationKeys(keys *KeyRing) *VerifyHandleBuilder { + vhb.handle.VerifyKeyRing = keys + return vhb +} + +// VerificationKey sets the public key for verifying the signatures. +func (vhb *VerifyHandleBuilder) VerificationKey(key *Key) *VerifyHandleBuilder { + var err error + if vhb.handle.VerifyKeyRing == nil { + vhb.handle.VerifyKeyRing, err = NewKeyRing(key) + } else { + err = vhb.handle.VerifyKeyRing.AddKey(key) + } + vhb.err = err + return vhb +} + +// VerificationContext sets a verification context for signatures of the pgp message, if any. +// Only considered if VerifyKeys are set. +func (vhb *VerifyHandleBuilder) VerificationContext(verifyContext *VerificationContext) *VerifyHandleBuilder { + vhb.handle.VerificationContext = verifyContext + return vhb +} + +// VerifyTime sets the verification time to the provided timestamp. +// If not set, the systems current time is used for signature verification. +func (vhb *VerifyHandleBuilder) VerifyTime(unixTime int64) *VerifyHandleBuilder { + vhb.handle.clock = NewConstantClock(unixTime) + return vhb +} + +// Utf8 indicates if the output plaintext is Utf8 and +// should be sanitized from canonicalised line endings. +// If enabled for detached verification, it canonicalises the input +// before verification independent of the signature type. +func (vhb *VerifyHandleBuilder) Utf8() *VerifyHandleBuilder { + vhb.handle.IsUTF8 = true + return vhb +} + +// DisableVerifyTimeCheck disables the check for comparing the signature expiration time +// against the verification time. +func (vhb *VerifyHandleBuilder) DisableVerifyTimeCheck() *VerifyHandleBuilder { + vhb.handle.DisableVerifyTimeCheck = true + return vhb +} + +// DisableStrictMessageParsing disables the check that the inputs conform +// to the OpenPGP message grammar. +// If set, no error is thrown if the input message does not conform to the +// OpenPGP specification. +func (vhb *VerifyHandleBuilder) DisableStrictMessageParsing() *VerifyHandleBuilder { + vhb.handle.DisableStrictMessageParsing = true + return vhb +} + +// DisableAutomaticTextSanitize indicates that automatic text sanitization should be disabled. +// If not disabled, the output will be sanitized if a text signature is present. +func (vhb *VerifyHandleBuilder) DisableAutomaticTextSanitize() *VerifyHandleBuilder { + vhb.handle.DisableAutomaticTextSanitize = true + return vhb +} + +// New creates a VerifyHandle and checks that the given +// combination of parameters is valid. If the parameters are invalid, +// an error is returned. +func (vhb *VerifyHandleBuilder) New() (PGPVerify, error) { + if vhb.err != nil { + return nil, vhb.err + } + vhb.err = vhb.handle.validate() + if vhb.err != nil { + return nil, vhb.err + } + handle := vhb.handle + vhb.handle = defaultVerifyHandle(vhb.profile, vhb.defaultClock) + return handle, nil +} + +// Error returns any errors that occurred within the builder. +func (vhb *VerifyHandleBuilder) Error() error { + return vhb.err +} diff --git a/crypto/verify_reader.go b/crypto/verify_reader.go new file mode 100644 index 00000000..8163367f --- /dev/null +++ b/crypto/verify_reader.go @@ -0,0 +1,148 @@ +package crypto + +import ( + "io" + + openpgp "github.com/ProtonMail/go-crypto/openpgp/v2" + "github.com/pkg/errors" +) + +// VerifyDataReader is used for reading data that should be verified with a signature. +// It further contains additional information about the parsed pgp message where the read +// data stems from. +type VerifyDataReader struct { + details *openpgp.MessageDetails + internalReader Reader + verifyKeyRing *KeyRing + verifyTime int64 + disableTimeCheck bool + readAll bool + verificationContext *VerificationContext +} + +// GetMetadata returns the metadata of the literal data packet that +// this reader reads from. Can be nil, if the data is not read from +// a literal data packet. +func (msg *VerifyDataReader) GetMetadata() *LiteralMetadata { + if msg.details.LiteralData == nil { + return nil + } + return &LiteralMetadata{ + filename: msg.details.LiteralData.FileName, + isUTF8: !msg.details.LiteralData.IsBinary, + ModTime: int64(msg.details.LiteralData.Time), + } +} + +// Read is used read data from the pgp message. +// Makes VerifyDataReader implement the Reader interface. +func (msg *VerifyDataReader) Read(b []byte) (n int, err error) { + n, err = msg.internalReader.Read(b) + if errors.Is(err, io.EOF) { + msg.readAll = true + } + return +} + +// VerifySignature is used to verify that the embedded signatures are valid. +// This method needs to be called once all the data has been read. +// It will return an error if the signature is invalid, no verifying keys are accessible, +// or if the message hasn't been read entirely. +func (msg *VerifyDataReader) VerifySignature() (result *VerifyResult, err error) { + if !msg.readAll { + return nil, errors.New("gopenpgp: can't verify the signature until the message reader has been read entirely") + } + return createVerifyResult(msg.details, msg.verifyKeyRing, msg.verificationContext, msg.verifyTime, msg.disableTimeCheck) +} + +// ReadAll reads all plaintext data from the reader +// and returns it as a byte slice. +func (msg *VerifyDataReader) ReadAll() (plaintext []byte, err error) { + return io.ReadAll(msg) +} + +// DiscardAll reads all data from the reader and discards it. +func (msg *VerifyDataReader) DiscardAll() (err error) { + _, err = io.Copy(io.Discard, msg) + return err +} + +// DiscardAllAndVerifySignature reads all plaintext data from the reader but discards it. +// Returns a verification result for signature verification on the read data. +func (msg *VerifyDataReader) DiscardAllAndVerifySignature() (vr *VerifyResult, err error) { + err = msg.DiscardAll() + if err != nil { + return nil, errors.Wrap(err, "gopenpgp: discarding data from reader failed") + } + return msg.VerifySignature() +} + +// ReadAllAndVerifySignature reads all plaintext data from the reader +// and tries to verify the signatures included in the message. +// Returns the data in a VerifiedDataResult struct, which can be checked for signature errors. +func (msg *VerifyDataReader) ReadAllAndVerifySignature() (*VerifiedDataResult, error) { + plaintext, err := msg.ReadAll() + if err != nil { + return nil, errors.Wrap(err, "gopenpgp: reading all data from reader failed") + } + verifyResult, err := msg.VerifySignature() + return &VerifiedDataResult{ + VerifyResult: *verifyResult, + data: plaintext, + metadata: msg.GetMetadata(), + cachedSessionKey: msg.SessionKey(), + }, err +} + +// SessionKey returns the session key the data is decrypted with. +// Returns nil, if this reader does not read from an encrypted message or +// session key caching was not enabled. +func (msg *VerifyDataReader) SessionKey() *SessionKey { + if msg.details.SessionKey == nil { + return nil + } + alg := getAlgo(msg.details.DecryptedWithAlgorithm) + return NewSessionKeyFromToken(msg.details.SessionKey, alg) +} + +// VerifiedDataResult is a result that contains data and +// the result of a potential signature verification on the data. +type VerifiedDataResult struct { + VerifyResult + metadata *LiteralMetadata + data []byte + cachedSessionKey *SessionKey +} + +// Metadata returns the associated literal metadata of the data. +func (r *VerifiedDataResult) Metadata() *LiteralMetadata { + return r.metadata +} + +// Bytes returns the result data as bytes. +func (r *VerifiedDataResult) Bytes() []byte { + return r.data +} + +// String returns the result data as string. +func (r *VerifiedDataResult) String() string { + return string(r.data) +} + +// SessionKey returns the session key the data is decrypted with. +// Returns nil, if the data was not encrypted or +// session key caching was not enabled. +func (r *VerifiedDataResult) SessionKey() *SessionKey { + return r.cachedSessionKey +} + +// VerifyCleartextResult is a result of a cleartext message verification. +type VerifyCleartextResult struct { + VerifyResult + cleartext []byte +} + +// Cleartext returns the parsed plain text of the result. +func (vc *VerifyCleartextResult) Cleartext() []byte { + return vc.cleartext +} diff --git a/go.mod b/go.mod index 8d21b6cc..34383e78 100644 --- a/go.mod +++ b/go.mod @@ -1,20 +1,22 @@ -module github.com/ProtonMail/gopenpgp/v2 +module github.com/ProtonMail/gopenpgp/v3 go 1.17 require ( - github.com/ProtonMail/go-crypto v1.1.0-alpha.1 + github.com/ProtonMail/go-crypto v1.1.0-alpha.5 github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.7.0 - golang.org/x/crypto v0.17.0 ) require ( github.com/cloudflare/circl v1.3.7 // indirect github.com/davecgh/go-spew v1.1.0 // indirect + github.com/kr/pretty v0.2.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/crypto v0.17.0 // indirect golang.org/x/sys v0.16.0 // indirect golang.org/x/text v0.14.0 // indirect + gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect ) diff --git a/go.sum b/go.sum index 260c5803..934d5559 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/ProtonMail/go-crypto v1.1.0-alpha.1 h1:iKLDnKGL+3u4Q5OjYgixAxWdkkGBPidCQumqVryUgtY= -github.com/ProtonMail/go-crypto v1.1.0-alpha.1/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/ProtonMail/go-crypto v1.1.0-alpha.5 h1:XdtNqgUwXGClX/O+nSxecHUpgbsOGaUyv22y6p1laNk= +github.com/ProtonMail/go-crypto v1.1.0-alpha.5/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k= github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= @@ -7,6 +7,11 @@ github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vc github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -57,7 +62,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/helper/base_test.go b/helper/base_test.go deleted file mode 100644 index d3fd8c18..00000000 --- a/helper/base_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package helper - -import ( - "io/ioutil" - "strings" - - "github.com/ProtonMail/gopenpgp/v2/crypto" -) - -const testTime = 1557754627 // 2019-05-13T13:37:07+00:00 - -func readTestFile(name string, trimNewlines bool) string { - data, err := ioutil.ReadFile("../crypto/testdata/" + name) //nolint - if err != nil { - panic(err) - } - if trimNewlines { - return strings.TrimRight(string(data), "\n") - } - return string(data) -} - -// Corresponding key in ../crypto/testdata/keyring_privateKey. -var testMailboxPassword = []byte("apple") - -func init() { - crypto.UpdateTime(testTime) // 2019-05-13T13:37:07+00:00 -} diff --git a/helper/cleartext.go b/helper/cleartext.go deleted file mode 100644 index d5517b16..00000000 --- a/helper/cleartext.go +++ /dev/null @@ -1,79 +0,0 @@ -package helper - -import ( - "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/ProtonMail/gopenpgp/v2/internal" - "github.com/pkg/errors" -) - -// SignCleartextMessageArmored signs text given a private key and its -// passphrase, canonicalizes and trims the newlines, and returns the -// PGP-compliant special armoring. -func SignCleartextMessageArmored(privateKey string, passphrase []byte, text string) (string, error) { - signingKey, err := crypto.NewKeyFromArmored(privateKey) - if err != nil { - return "", errors.Wrap(err, "gopenpgp: error in creating key object") - } - - unlockedKey, err := signingKey.Unlock(passphrase) - if err != nil { - return "", errors.Wrap(err, "gopenpgp: error in unlocking key") - } - defer unlockedKey.ClearPrivateParams() - - keyRing, err := crypto.NewKeyRing(unlockedKey) - if err != nil { - return "", errors.Wrap(err, "gopenpgp: error in creating keyring") - } - - return SignCleartextMessage(keyRing, text) -} - -// VerifyCleartextMessageArmored verifies PGP-compliant armored signed plain -// text given the public key and returns the text or err if the verification -// fails. -func VerifyCleartextMessageArmored(publicKey, armored string, verifyTime int64) (string, error) { - signingKey, err := crypto.NewKeyFromArmored(publicKey) - if err != nil { - return "", errors.Wrap(err, "gopenpgp: error in creating key object") - } - - verifyKeyRing, err := crypto.NewKeyRing(signingKey) - if err != nil { - return "", errors.Wrap(err, "gopenpgp: error in creating key ring") - } - - return VerifyCleartextMessage(verifyKeyRing, armored, verifyTime) -} - -// SignCleartextMessage signs text given a private keyring, canonicalizes and -// trims the newlines, and returns the PGP-compliant special armoring. -func SignCleartextMessage(keyRing *crypto.KeyRing, text string) (string, error) { - message := crypto.NewPlainMessageFromString(internal.TrimEachLine(text)) - - signature, err := keyRing.SignDetached(message) - if err != nil { - return "", errors.Wrap(err, "gopenpgp: error in signing cleartext message") - } - - return crypto.NewClearTextMessage(message.GetBinary(), signature.GetBinary()).GetArmored() -} - -// VerifyCleartextMessage verifies PGP-compliant armored signed plain text -// given the public keyring and returns the text or err if the verification -// fails. -func VerifyCleartextMessage(keyRing *crypto.KeyRing, armored string, verifyTime int64) (string, error) { - clearTextMessage, err := crypto.NewClearTextMessageFromArmored(armored) - if err != nil { - return "", errors.Wrap(err, "gopengpp: unable to unarmor cleartext message") - } - - message := crypto.NewPlainMessageFromString(internal.TrimEachLine(clearTextMessage.GetString())) - signature := crypto.NewPGPSignature(clearTextMessage.GetBinarySignature()) - err = keyRing.VerifyDetached(message, signature, verifyTime) - if err != nil { - return "", errors.Wrap(err, "gopengpp: unable to verify cleartext message") - } - - return message.GetString(), nil -} diff --git a/helper/cleartext_test.go b/helper/cleartext_test.go deleted file mode 100644 index 1e39ff94..00000000 --- a/helper/cleartext_test.go +++ /dev/null @@ -1,49 +0,0 @@ -package helper - -import ( - "regexp" - "testing" - - "github.com/ProtonMail/gopenpgp/v2/internal" - - "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/stretchr/testify/assert" -) - -const inputPlainText = " Signed message\n \n " -const signedPlainText = " Signed message\n\n" - -var signedMessageTest = regexp.MustCompile( - "(?s)^-----BEGIN PGP SIGNED MESSAGE-----.*-----BEGIN PGP SIGNATURE-----.*-----END PGP SIGNATURE-----$") - -func TestSignClearText(t *testing.T) { - // Password defined in base_test - armored, err := SignCleartextMessageArmored( - readTestFile("keyring_privateKey", false), - testMailboxPassword, - inputPlainText, - ) - - if err != nil { - t.Fatal("Cannot armor message:", err) - } - - assert.Regexp(t, signedMessageTest, armored) - - verified, err := VerifyCleartextMessageArmored( - readTestFile("keyring_publicKey", false), - armored, - crypto.GetUnixTime(), - ) - if err != nil { - t.Fatal("Cannot verify message:", err) - } - - assert.Exactly(t, signedPlainText, verified) - - clearTextMessage, err := crypto.NewClearTextMessageFromArmored(armored) - if err != nil { - t.Fatal("Cannot parse message:", err) - } - assert.Exactly(t, internal.Canonicalize(internal.TrimEachLine(inputPlainText)), string(clearTextMessage.GetBinary())) -} diff --git a/helper/decrypt_check.go b/helper/decrypt_check.go deleted file mode 100644 index eeff1089..00000000 --- a/helper/decrypt_check.go +++ /dev/null @@ -1,86 +0,0 @@ -package helper - -import ( - "bytes" - "crypto/aes" - "crypto/cipher" - "io" - - "github.com/ProtonMail/go-crypto/openpgp/packet" - "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/pkg/errors" -) - -const AES_BLOCK_SIZE = 16 - -func supported(cipher packet.CipherFunction) bool { - switch cipher { - case packet.CipherAES128, packet.CipherAES192, packet.CipherAES256: - return true - case packet.CipherCAST5, packet.Cipher3DES: - return false - } - return false -} - -func blockSize(cipher packet.CipherFunction) int { - switch cipher { - case packet.CipherAES128, packet.CipherAES192, packet.CipherAES256: - return AES_BLOCK_SIZE - case packet.CipherCAST5, packet.Cipher3DES: - return 0 - } - return 0 -} - -func blockCipher(cipher packet.CipherFunction, key []byte) (cipher.Block, error) { - switch cipher { - case packet.CipherAES128, packet.CipherAES192, packet.CipherAES256: - return aes.NewCipher(key) - case packet.CipherCAST5, packet.Cipher3DES: - return nil, errors.New("gopenpgp: cipher not supported for quick check") - } - return nil, errors.New("gopenpgp: unknown cipher") -} - -// QuickCheckDecryptReader checks with high probability if the provided session key -// can decrypt a data packet given its 24 byte long prefix. -// The method reads up to but not exactly 24 bytes from the prefixReader. -// NOTE: Only works for SEIPDv1 packets with AES. -func QuickCheckDecryptReader(sessionKey *crypto.SessionKey, prefixReader crypto.Reader) (bool, error) { - algo, err := sessionKey.GetCipherFunc() - if err != nil { - return false, errors.New("gopenpgp: cipher algorithm not found") - } - if !supported(algo) { - return false, errors.New("gopenpgp: cipher not supported for quick check") - } - packetParser := packet.NewReader(prefixReader) - _, err = packetParser.Next() - if err != nil { - return false, errors.New("gopenpgp: failed to parse packet prefix") - } - - blockSize := blockSize(algo) - encryptedData := make([]byte, blockSize+2) - _, err = io.ReadFull(prefixReader, encryptedData) - if err != nil { - return false, errors.New("gopenpgp: prefix is too short to check") - } - - blockCipher, err := blockCipher(algo, sessionKey.Key) - if err != nil { - return false, errors.New("gopenpgp: failed to initialize the cipher") - } - _ = packet.NewOCFBDecrypter(blockCipher, encryptedData, packet.OCFBNoResync) - return encryptedData[blockSize-2] == encryptedData[blockSize] && - encryptedData[blockSize-1] == encryptedData[blockSize+1], nil -} - -// QuickCheckDecrypt checks with high probability if the provided session key -// can decrypt the encrypted data packet given its 24 byte long prefix. -// The method only considers the first 24 bytes of the prefix slice (prefix[:24]). -// NOTE: Only works for SEIPDv1 packets with AES. -func QuickCheckDecrypt(sessionKey *crypto.SessionKey, prefix []byte) (bool, error) { - return QuickCheckDecryptReader(sessionKey, bytes.NewReader(prefix)) -} diff --git a/helper/decrypt_check_test.go b/helper/decrypt_check_test.go deleted file mode 100644 index f7341114..00000000 --- a/helper/decrypt_check_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package helper - -import ( - "encoding/hex" - "testing" - - "github.com/ProtonMail/gopenpgp/v2/crypto" -) - -const testQuickCheckSessionKey = `038c9cb9d408074e36bac22c6b90973082f86e5b01f38b787da3927000365a81` -const testQuickCheckSessionKeyAlg = "aes256" -const testQuickCheckDataPacket = `d2540152ab2518950f282d98d901eb93c00fb55a3bb30b3b517d6a356f57884bac6963060ebb167ffc3296e5e99ec058aeff5003a4784a0734a62861ae56d2921b9b790d50586cd21cad45e2d84ac93fb5d8af2ce6c5` - -func TestCheckDecrypt(t *testing.T) { - sessionKeyData, err := hex.DecodeString(testQuickCheckSessionKey) - if err != nil { - t.Error(err) - } - dataPacket, err := hex.DecodeString(testQuickCheckDataPacket) - if err != nil { - t.Error(err) - } - sessionKey := &crypto.SessionKey{ - Key: sessionKeyData, - Algo: testQuickCheckSessionKeyAlg, - } - ok, err := QuickCheckDecrypt(sessionKey, dataPacket[:22]) - if err != nil { - t.Error(err) - } - if !ok { - t.Error("should be able to decrypt") - } - - sessionKey.Key[0] += 1 - ok, err = QuickCheckDecrypt(sessionKey, dataPacket[:22]) - if err != nil { - t.Error(err) - } - if ok { - t.Error("should no be able to decrypt") - } -} diff --git a/helper/helper.go b/helper/helper.go deleted file mode 100644 index 76c1f123..00000000 --- a/helper/helper.go +++ /dev/null @@ -1,600 +0,0 @@ -// Package helper contains several functions with a simple interface to extend usability and compatibility with gomobile -package helper - -import ( - "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/pkg/errors" -) - -// EncryptMessageWithPassword encrypts a string with a passphrase using AES256. -func EncryptMessageWithPassword(password []byte, plaintext string) (ciphertext string, err error) { - var pgpMessage *crypto.PGPMessage - - var message = crypto.NewPlainMessageFromString(plaintext) - - if pgpMessage, err = crypto.EncryptMessageWithPassword(message, password); err != nil { - return "", errors.Wrap(err, "gopenpgp: unable to encrypt message with password") - } - - if ciphertext, err = pgpMessage.GetArmored(); err != nil { - return "", errors.Wrap(err, "gopenpgp: unable to armor ciphertext") - } - - return ciphertext, nil -} - -// DecryptMessageWithPassword decrypts an armored message with a random token. -// The algorithm is derived from the armoring. -func DecryptMessageWithPassword(password []byte, ciphertext string) (plaintext string, err error) { - var message *crypto.PlainMessage - var pgpMessage *crypto.PGPMessage - - if pgpMessage, err = crypto.NewPGPMessageFromArmored(ciphertext); err != nil { - return "", errors.Wrap(err, "gopenpgp: unable to unarmor ciphertext") - } - - if message, err = crypto.DecryptMessageWithPassword(pgpMessage, password); err != nil { - return "", errors.Wrap(err, "gopenpgp: unable to decrypt message with password") - } - - return message.GetString(), nil -} - -// EncryptMessageArmored generates an armored PGP message given a plaintext and -// an armored public key. -func EncryptMessageArmored(key, plaintext string) (string, error) { - return encryptMessageArmored(key, crypto.NewPlainMessageFromString(plaintext)) -} - -// EncryptSignMessageArmored generates an armored signed PGP message given a -// plaintext and an armored public key a private key and its passphrase. -func EncryptSignMessageArmored( - publicKey, privateKey string, passphrase []byte, plaintext string, -) (ciphertext string, err error) { - var privateKeyObj, unlockedKeyObj *crypto.Key - var publicKeyRing, privateKeyRing *crypto.KeyRing - var pgpMessage *crypto.PGPMessage - - var message = crypto.NewPlainMessageFromString(plaintext) - - if publicKeyRing, err = createPublicKeyRing(publicKey); err != nil { - return "", err - } - - if privateKeyObj, err = crypto.NewKeyFromArmored(privateKey); err != nil { - return "", errors.Wrap(err, "gopenpgp: unable to read key") - } - - if unlockedKeyObj, err = privateKeyObj.Unlock(passphrase); err != nil { - return "", errors.Wrap(err, "gopenpgp: unable to unlock key") - } - defer unlockedKeyObj.ClearPrivateParams() - - if privateKeyRing, err = crypto.NewKeyRing(unlockedKeyObj); err != nil { - return "", errors.Wrap(err, "gopenpgp: unable to create new keyring") - } - - if pgpMessage, err = publicKeyRing.Encrypt(message, privateKeyRing); err != nil { - return "", errors.Wrap(err, "gopenpgp: unable to encrypt message") - } - - if ciphertext, err = pgpMessage.GetArmored(); err != nil { - return "", errors.Wrap(err, "gopenpgp: unable to armor ciphertext") - } - - return ciphertext, nil -} - -// DecryptMessageArmored decrypts an armored PGP message given a private key -// and its passphrase. -func DecryptMessageArmored( - privateKey string, passphrase []byte, ciphertext string, -) (string, error) { - message, err := decryptMessageArmored(privateKey, passphrase, ciphertext) - if err != nil { - return "", err - } - - return message.GetString(), nil -} - -// DecryptVerifyMessageArmored decrypts an armored PGP message given a private -// key and its passphrase and verifies the embedded signature. Returns the -// plain data or an error on signature verification failure. -func DecryptVerifyMessageArmored( - publicKey, privateKey string, passphrase []byte, ciphertext string, -) (plaintext string, err error) { - var privateKeyObj, unlockedKeyObj *crypto.Key - var publicKeyRing, privateKeyRing *crypto.KeyRing - var pgpMessage *crypto.PGPMessage - var message *crypto.PlainMessage - - if publicKeyRing, err = createPublicKeyRing(publicKey); err != nil { - return "", err - } - - if privateKeyObj, err = crypto.NewKeyFromArmored(privateKey); err != nil { - return "", errors.Wrap(err, "gopenpgp: unable to unarmor private key") - } - - if unlockedKeyObj, err = privateKeyObj.Unlock(passphrase); err != nil { - return "", errors.Wrap(err, "gopenpgp: unable to unlock private key") - } - defer unlockedKeyObj.ClearPrivateParams() - - if privateKeyRing, err = crypto.NewKeyRing(unlockedKeyObj); err != nil { - return "", errors.Wrap(err, "gopenpgp: unable to create new keyring") - } - - if pgpMessage, err = crypto.NewPGPMessageFromArmored(ciphertext); err != nil { - return "", errors.Wrap(err, "gopenpgp: unable to unarmor ciphertext") - } - - if message, err = privateKeyRing.Decrypt(pgpMessage, publicKeyRing, crypto.GetUnixTime()); err != nil { - return "", errors.Wrap(err, "gopenpgp: unable to decrypt message") - } - - return message.GetString(), nil -} - -// DecryptVerifyAttachment decrypts and verifies an attachment split into the -// keyPacket, dataPacket and an armored (!) signature, given a publicKey, and a -// privateKey with its passphrase. Returns the plain data or an error on -// signature verification failure. -func DecryptVerifyAttachment( - publicKey, privateKey string, - passphrase, keyPacket, dataPacket []byte, - armoredSignature string, -) (plainData []byte, err error) { - // We decrypt the attachment - message, err := decryptAttachment(privateKey, passphrase, keyPacket, dataPacket) - if err != nil { - return nil, err - } - - // We verify the signature - var check bool - if check, err = verifyDetachedArmored(publicKey, message, armoredSignature); err != nil { - return nil, err - } - if !check { - return nil, errors.New("gopenpgp: unable to verify attachment") - } - - return message.GetBinary(), nil -} - -// EncryptBinaryMessageArmored generates an armored PGP message given a binary data and -// an armored public key. -func EncryptBinaryMessageArmored(key string, data []byte) (string, error) { - return encryptMessageArmored(key, crypto.NewPlainMessage(data)) -} - -// DecryptBinaryMessageArmored decrypts an armored PGP message given a private key -// and its passphrase. -func DecryptBinaryMessageArmored(privateKey string, passphrase []byte, ciphertext string) ([]byte, error) { - message, err := decryptMessageArmored(privateKey, passphrase, ciphertext) - if err != nil { - return nil, err - } - - return message.GetBinary(), nil -} - -// encryptSignArmoredDetached takes a public key for encryption, -// a private key and its passphrase for signature, and the plaintext data -// Returns an armored ciphertext and a detached armored encrypted signature. -func encryptSignArmoredDetached( - publicKey, privateKey string, - passphrase, plainData []byte, -) (ciphertextArmored, encryptedSignatureArmored string, err error) { - var message = crypto.NewPlainMessage(plainData) - - // We encrypt and signcrypt - ciphertext, encryptedSignatureArmored, err := encryptSignObjDetached(publicKey, privateKey, passphrase, message) - if err != nil { - return "", "", err - } - - // We armor the ciphertext and signature - ciphertextArmored, err = ciphertext.GetArmored() - if err != nil { - return "", "", errors.Wrap(err, "gopenpgp: unable to armor the ciphertext") - } - - return ciphertextArmored, encryptedSignatureArmored, nil -} - -// DecryptVerifyArmoredDetached decrypts an armored pgp message -// and verify a detached armored encrypted signature -// given a publicKey, and a privateKey with its passphrase. -// Returns the plain data or an error on -// signature verification failure. -func DecryptVerifyArmoredDetached( - publicKey, privateKey string, - passphrase []byte, - ciphertextArmored string, - encryptedSignatureArmored string, -) (plainData []byte, err error) { - // Some type casting - ciphertext, err := crypto.NewPGPMessageFromArmored(ciphertextArmored) - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: unable to unarmor ciphertext") - } - - // We decrypt and verify the encrypted signature - message, err := decryptVerifyObjDetached(publicKey, privateKey, passphrase, ciphertext, encryptedSignatureArmored) - if err != nil { - return nil, err - } - return message.GetBinary(), nil -} - -// encryptSignBinaryDetached takes a public key for encryption, -// a private key and its passphrase for signature, and the plaintext data -// Returns encrypted binary data and a detached armored encrypted signature. -func encryptSignBinaryDetached( - publicKey, privateKey string, - passphrase, plainData []byte, -) (encryptedData []byte, encryptedSignatureArmored string, err error) { - // Some type casting - message := crypto.NewPlainMessage(plainData) - - // We encrypt and signcrypt - ciphertext, encryptedSignatureArmored, err := encryptSignObjDetached(publicKey, privateKey, passphrase, message) - if err != nil { - return nil, "", err - } - - // We get the encrypted data - encryptedData = ciphertext.GetBinary() - return encryptedData, encryptedSignatureArmored, nil -} - -// DecryptVerifyBinaryDetached decrypts binary encrypted data -// and verify a detached armored encrypted signature -// given a publicKey, and a privateKey with its passphrase. -// Returns the plain data or an error on -// signature verification failure. -func DecryptVerifyBinaryDetached( - publicKey, privateKey string, - passphrase []byte, - encryptedData []byte, - encryptedSignatureArmored string, -) (plainData []byte, err error) { - // Some type casting - ciphertext := crypto.NewPGPMessage(encryptedData) - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: unable to parse ciphertext") - } - - // We decrypt and verify the encrypted signature - message, err := decryptVerifyObjDetached(publicKey, privateKey, passphrase, ciphertext, encryptedSignatureArmored) - if err != nil { - return nil, err - } - return message.GetBinary(), nil -} - -// EncryptAttachmentWithKey encrypts a binary file -// Using a given armored public key. -func EncryptAttachmentWithKey( - publicKey string, - filename string, - plainData []byte, -) (message *crypto.PGPSplitMessage, err error) { - publicKeyRing, err := createPublicKeyRing(publicKey) - if err != nil { - return nil, err - } - return EncryptAttachment(plainData, filename, publicKeyRing) -} - -// DecryptAttachmentWithKey decrypts a binary file -// Using a given armored private key and its passphrase. -func DecryptAttachmentWithKey( - privateKey string, - passphrase, keyPacket, dataPacket []byte, -) (attachment []byte, err error) { - message, err := decryptAttachment(privateKey, passphrase, keyPacket, dataPacket) - if err != nil { - return nil, err - } - return message.GetBinary(), nil -} - -// EncryptSessionKey encrypts a session key -// using a given armored public key. -func EncryptSessionKey( - publicKey string, - sessionKey *crypto.SessionKey, -) (encryptedSessionKey []byte, err error) { - publicKeyRing, err := createPublicKeyRing(publicKey) - if err != nil { - return nil, err - } - encryptedSessionKey, err = publicKeyRing.EncryptSessionKey(sessionKey) - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: unable to encrypt sessionKey") - } - return encryptedSessionKey, nil -} - -// DecryptSessionKey decrypts a session key -// using a given armored private key -// and its passphrase. -func DecryptSessionKey( - privateKey string, - passphrase, encryptedSessionKey []byte, -) (sessionKey *crypto.SessionKey, err error) { - privateKeyObj, err := crypto.NewKeyFromArmored(privateKey) - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: unable to read armored key") - } - - privateKeyUnlocked, err := privateKeyObj.Unlock(passphrase) - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: unable to unlock private key") - } - - defer privateKeyUnlocked.ClearPrivateParams() - - privateKeyRing, err := crypto.NewKeyRing(privateKeyUnlocked) - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: unable to create new keyring") - } - - sessionKey, err = privateKeyRing.DecryptSessionKey(encryptedSessionKey) - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: unable to decrypt session key") - } - - return sessionKey, nil -} - -func encryptMessageArmored(key string, message *crypto.PlainMessage) (string, error) { - ciphertext, err := encryptMessage(key, message) - if err != nil { - return "", err - } - - ciphertextArmored, err := ciphertext.GetArmored() - if err != nil { - return "", errors.Wrap(err, "gopenpgp: unable to armor ciphertext") - } - - return ciphertextArmored, nil -} - -func decryptMessageArmored(privateKey string, passphrase []byte, ciphertextArmored string) (*crypto.PlainMessage, error) { - ciphertext, err := crypto.NewPGPMessageFromArmored(ciphertextArmored) - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: unable to parse ciphertext") - } - - return decryptMessage(privateKey, passphrase, ciphertext) -} - -func encryptMessage(key string, message *crypto.PlainMessage) (*crypto.PGPMessage, error) { - publicKeyRing, err := createPublicKeyRing(key) - if err != nil { - return nil, err - } - - ciphertext, err := publicKeyRing.Encrypt(message, nil) - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: unable to encrypt message") - } - - return ciphertext, nil -} - -func decryptMessage(privateKey string, passphrase []byte, ciphertext *crypto.PGPMessage) (*crypto.PlainMessage, error) { - privateKeyObj, err := crypto.NewKeyFromArmored(privateKey) - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: unable to parse the private key") - } - - privateKeyUnlocked, err := privateKeyObj.Unlock(passphrase) - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: unable to unlock key") - } - - defer privateKeyUnlocked.ClearPrivateParams() - - privateKeyRing, err := crypto.NewKeyRing(privateKeyUnlocked) - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: unable to create the private key ring") - } - - message, err := privateKeyRing.Decrypt(ciphertext, nil, 0) - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: unable to decrypt message") - } - - return message, nil -} - -func signDetached(privateKey string, passphrase []byte, message *crypto.PlainMessage) (detachedSignature *crypto.PGPSignature, err error) { - privateKeyObj, err := crypto.NewKeyFromArmored(privateKey) - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: unable to parse private key") - } - - privateKeyUnlocked, err := privateKeyObj.Unlock(passphrase) - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: unable to unlock key") - } - - defer privateKeyUnlocked.ClearPrivateParams() - - privateKeyRing, err := crypto.NewKeyRing(privateKeyUnlocked) - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: unable to create new keyring") - } - - detachedSignature, err = privateKeyRing.SignDetached(message) - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: unable to sign message") - } - - return detachedSignature, nil -} - -func verifyDetachedArmored(publicKey string, message *crypto.PlainMessage, armoredSignature string) (check bool, err error) { - var detachedSignature *crypto.PGPSignature - - // We unarmor the signature - if detachedSignature, err = crypto.NewPGPSignatureFromArmored(armoredSignature); err != nil { - return false, errors.Wrap(err, "gopenpgp: unable to unarmor signature") - } - // we verify the signature - return verifyDetached(publicKey, message, detachedSignature) -} - -func verifyDetached(publicKey string, message *crypto.PlainMessage, detachedSignature *crypto.PGPSignature) (check bool, err error) { - var publicKeyRing *crypto.KeyRing - - // We prepare the public key for signature verification - publicKeyRing, err = createPublicKeyRing(publicKey) - if err != nil { - return false, err - } - - // We verify the signature - if publicKeyRing.VerifyDetached(message, detachedSignature, crypto.GetUnixTime()) != nil { - return false, nil - } - return true, nil -} - -func decryptAttachment( - privateKey string, - passphrase, keyPacket, dataPacket []byte, -) (message *crypto.PlainMessage, err error) { - var privateKeyObj, unlockedKeyObj *crypto.Key - var privateKeyRing *crypto.KeyRing - - packets := crypto.NewPGPSplitMessage(keyPacket, dataPacket) - - // prepare the private key for decryption - if privateKeyObj, err = crypto.NewKeyFromArmored(privateKey); err != nil { - return nil, errors.Wrap(err, "gopenpgp: unable to parse private key") - } - if unlockedKeyObj, err = privateKeyObj.Unlock(passphrase); err != nil { - return nil, errors.Wrap(err, "gopenpgp: unable to unlock private key") - } - defer unlockedKeyObj.ClearPrivateParams() - - if privateKeyRing, err = crypto.NewKeyRing(unlockedKeyObj); err != nil { - return nil, errors.Wrap(err, "gopenpgp: unable to create new keyring") - } - - if message, err = privateKeyRing.DecryptAttachment(packets); err != nil { - return nil, errors.Wrap(err, "gopenpgp: unable to decrypt attachment") - } - - return message, nil -} - -func createPublicKeyRing(publicKey string) (*crypto.KeyRing, error) { - publicKeyObj, err := crypto.NewKeyFromArmored(publicKey) - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: unable to parse public key") - } - - if publicKeyObj.IsPrivate() { - publicKeyObj, err = publicKeyObj.ToPublic() - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: unable to extract public key from private key") - } - } - - publicKeyRing, err := crypto.NewKeyRing(publicKeyObj) - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: unable to create new keyring") - } - - return publicKeyRing, nil -} - -func encryptSignObjDetached( - publicKey, privateKey string, - passphrase []byte, - message *crypto.PlainMessage, -) (ciphertext *crypto.PGPSplitMessage, encryptedSignatureArmored string, err error) { - // We generate the session key - sessionKey, err := crypto.GenerateSessionKey() - if err != nil { - return nil, "", errors.Wrap(err, "gopenpgp: unable to create new session key") - } - - // We encrypt the message with the session key - messageDataPacket, err := sessionKey.Encrypt(message) - if err != nil { - return nil, "", errors.Wrap(err, "gopenpgp: unable to encrypt message") - } - - // We sign the message - detachedSignature, err := signDetached(privateKey, passphrase, message) - if err != nil { - return nil, "", errors.Wrap(err, "gopenpgp: unable to sign message") - } - - // We encrypt the signature with the session key - signaturePlaintext := crypto.NewPlainMessage(detachedSignature.GetBinary()) - signatureDataPacket, err := sessionKey.Encrypt(signaturePlaintext) - if err != nil { - return nil, "", errors.Wrap(err, "gopenpgp: unable to encrypt detached signature") - } - - // We encrypt the session key - keyPacket, err := EncryptSessionKey(publicKey, sessionKey) - if err != nil { - return nil, "", errors.Wrap(err, "gopenpgp: unable to encrypt the session key") - } - - // We join the key packets and datapackets - ciphertext = crypto.NewPGPSplitMessage(keyPacket, messageDataPacket) - encryptedSignature := crypto.NewPGPSplitMessage(keyPacket, signatureDataPacket) - encryptedSignatureArmored, err = encryptedSignature.GetArmored() - if err != nil { - return nil, "", errors.Wrap(err, "gopenpgp: unable to armor encrypted signature") - } - return ciphertext, encryptedSignatureArmored, nil -} - -func decryptVerifyObjDetached( - publicKey, privateKey string, - passphrase []byte, - ciphertext *crypto.PGPMessage, - encryptedSignatureArmored string, -) (message *crypto.PlainMessage, err error) { - // We decrypt the message - if message, err = decryptMessage(privateKey, passphrase, ciphertext); err != nil { - return nil, err - } - - encryptedSignature, err := crypto.NewPGPMessageFromArmored(encryptedSignatureArmored) - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: unable to parse encrypted signature") - } - - // We decrypt the signature - signatureMessage, err := decryptMessage(privateKey, passphrase, encryptedSignature) - if err != nil { - return nil, err - } - detachedSignature := crypto.NewPGPSignature(signatureMessage.GetBinary()) - - // We verify the signature - var check bool - if check, err = verifyDetached(publicKey, message, detachedSignature); err != nil { - return nil, err - } - if !check { - return nil, errors.New("gopenpgp: unable to verify message") - } - - return message, nil -} diff --git a/helper/helper_test.go b/helper/helper_test.go deleted file mode 100644 index 63bc5fb8..00000000 --- a/helper/helper_test.go +++ /dev/null @@ -1,354 +0,0 @@ -package helper - -import ( - "bytes" - "testing" - - "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/stretchr/testify/assert" -) - -func TestAESEncryption(t *testing.T) { - var plaintext = "Symmetric secret" - var passphrase = []byte("passphrase") - - ciphertext, err := EncryptMessageWithPassword(passphrase, plaintext) - if err != nil { - t.Fatal("Expected no error when encrypting, got:", err) - } - - _, err = DecryptMessageWithPassword([]byte("Wrong passphrase"), ciphertext) - assert.Containsf(t, err.Error(), "wrong password", "expected error containing 'wrong password', got %s", err) - - decrypted, err := DecryptMessageWithPassword(passphrase, ciphertext) - if err != nil { - t.Fatal("Expected no error when decrypting, got:", err) - } - - assert.Exactly(t, plaintext, decrypted) -} - -func TestArmoredTextMessageEncryption(t *testing.T) { - var plaintext = "Secret message" - - armored, err := EncryptMessageArmored(readTestFile("keyring_publicKey", false), plaintext) - if err != nil { - t.Fatal("Expected no error when encrypting, got:", err) - } - - assert.Exactly(t, true, crypto.IsPGPMessage(armored)) - - decrypted, err := DecryptMessageArmored( - readTestFile("keyring_privateKey", false), - testMailboxPassword, // Password defined in base_test - armored, - ) - if err != nil { - t.Fatal("Expected no error when decrypting, got:", err) - } - - assert.Exactly(t, plaintext, decrypted) -} - -func TestArmoredTextMessageEncryptionVerification(t *testing.T) { - var plaintext = "Secret message" - - armored, err := EncryptSignMessageArmored( - readTestFile("keyring_privateKey", false), - readTestFile("keyring_privateKey", false), - testMailboxPassword, // Password defined in base_test - plaintext, - ) - if err != nil { - t.Fatal("Expected no error when encrypting, got:", err) - } - - assert.Exactly(t, true, crypto.IsPGPMessage(armored)) - - _, err = DecryptVerifyMessageArmored( - readTestFile("mime_privateKey", false), // Wrong public key - readTestFile("keyring_privateKey", false), - testMailboxPassword, // Password defined in base_test - armored, - ) - assert.EqualError(t, err, "gopenpgp: unable to decrypt message: Signature Verification Error: No matching signature") - - decrypted, err := DecryptVerifyMessageArmored( - readTestFile("keyring_privateKey", false), - readTestFile("keyring_privateKey", false), - testMailboxPassword, // Password defined in base_test - armored, - ) - - if err != nil { - t.Fatal("Expected no error when decrypting, got:", err) - } - - assert.Exactly(t, plaintext, decrypted) -} - -func TestAttachmentEncryptionVerification(t *testing.T) { - var attachment = []byte("Secret file\r\nRoot password:hunter2") - - keyPacket, dataPacket, signature, err := EncryptSignAttachment( - readTestFile("keyring_privateKey", false), - readTestFile("keyring_privateKey", false), - testMailboxPassword, // Password defined in base_test - "password.txt", - attachment, - ) - if err != nil { - t.Fatal("Expected no error when encrypting, got:", err) - } - - sig := crypto.NewPGPSignature(signature) - armoredSig, err := sig.GetArmored() - if err != nil { - t.Fatal("Expected no error when armoring signature, got:", err) - } - - _, err = DecryptVerifyAttachment( - readTestFile("mime_privateKey", false), // Wrong public key - readTestFile("keyring_privateKey", false), - testMailboxPassword, // Password defined in base_test - keyPacket, - dataPacket, - armoredSig, - ) - assert.EqualError(t, err, "gopenpgp: unable to verify attachment") - - decrypted, err := DecryptVerifyAttachment( - readTestFile("keyring_privateKey", false), - readTestFile("keyring_privateKey", false), - testMailboxPassword, // Password defined in base_test - keyPacket, - dataPacket, - armoredSig, - ) - if err != nil { - t.Fatal("Expected no error when decrypting, got:", err) - } - - assert.Exactly(t, attachment, decrypted) -} - -func TestArmoredBinaryMessageEncryption(t *testing.T) { - plainData := []byte("Secret message") - - armored, err := EncryptBinaryMessageArmored(readTestFile("keyring_privateKey", false), plainData) - - if err != nil { - t.Fatal("Expected no error when encrypting, got:", err) - } - - assert.Exactly(t, true, crypto.IsPGPMessage(armored)) - - decrypted, err := DecryptBinaryMessageArmored( - readTestFile("keyring_privateKey", false), - testMailboxPassword, // Password defined in base_test - armored, - ) - - if err != nil { - t.Fatal("Expected no error when decrypting, got:", err) - } - - assert.Exactly(t, plainData, decrypted) -} - -func TestEncryptSignArmoredDetached(t *testing.T) { - plainData := []byte("Secret message") - privateKeyString := readTestFile("keyring_privateKey", false) - privateKey, err := crypto.NewKeyFromArmored(privateKeyString) - if err != nil { - t.Fatal("Error reading the test private key: ", err) - } - publicKeyString, err := privateKey.GetArmoredPublicKey() - if err != nil { - t.Fatal("Error reading the test public key: ", err) - } - armoredCiphertext, armoredSignature, err := EncryptSignArmoredDetached( - publicKeyString, - privateKeyString, - testMailboxPassword, // Password defined in base_test - plainData, - ) - if err != nil { - t.Fatal("Expected no error while encrypting and signing, got:", err) - } - - decrypted, err := DecryptVerifyArmoredDetached( - publicKeyString, - privateKeyString, - testMailboxPassword, - armoredCiphertext, - armoredSignature, - ) - - if err != nil { - t.Fatal("Expected no error while decrypting and verifying, got:", err) - } - - if !bytes.Equal(decrypted, plainData) { - t.Error("Decrypted is not equal to the plaintext") - } - - _, modifiedSignature, err := EncryptSignArmoredDetached( - publicKeyString, - privateKeyString, - testMailboxPassword, // Password defined in base_test - []byte("Different message"), - ) - - if err != nil { - t.Fatal("Expected no error while encrypting and signing, got:", err) - } - - _, err = DecryptVerifyArmoredDetached( - publicKeyString, - privateKeyString, - testMailboxPassword, - armoredCiphertext, - modifiedSignature, - ) - - if err == nil { - t.Fatal("Expected an error while decrypting and verifying with a wrong signature") - } -} - -func TestEncryptDecryptAttachmenWithKey(t *testing.T) { - plainData := []byte("Secret message") - privateKeyString := readTestFile("keyring_privateKey", false) - privateKey, err := crypto.NewKeyFromArmored(privateKeyString) - if err != nil { - t.Fatal("Error reading the test private key: ", err) - } - publicKeyString, err := privateKey.GetArmoredPublicKey() - if err != nil { - t.Fatal("Error reading the test public key: ", err) - } - pgpSplitMessage, err := EncryptAttachmentWithKey( - publicKeyString, - "test_filename", - plainData, - ) - - if err != nil { - t.Fatal("Expected no error while encrypting, got:", err) - } - - decrypted, err := DecryptAttachmentWithKey( - privateKeyString, - testMailboxPassword, - pgpSplitMessage.KeyPacket, - pgpSplitMessage.DataPacket, - ) - - if err != nil { - t.Fatal("Expected no error while decrypting, got:", err) - } - - if !bytes.Equal(decrypted, plainData) { - t.Error("Decrypted attachment is not equal to the original attachment") - } -} - -func TestEncryptDecryptSessionKey(t *testing.T) { - privateKeyString := readTestFile("keyring_privateKey", false) - privateKey, err := crypto.NewKeyFromArmored(privateKeyString) - if err != nil { - t.Fatal("Error reading the test private key: ", err) - } - publicKeyString, err := privateKey.GetArmoredPublicKey() - if err != nil { - t.Fatal("Error reading the test public key: ", err) - } - - sessionKey, err := crypto.GenerateSessionKeyAlgo("aes256") - - if err != nil { - t.Fatal("Expected no error while generating the session key, got:", err) - } - - encrypted, err := EncryptSessionKey(publicKeyString, sessionKey) - - if err != nil { - t.Fatal("Expected no error while encrypting session key, got:", err) - } - - decryptedSessionKey, err := DecryptSessionKey( - privateKeyString, - testMailboxPassword, - encrypted, - ) - - if err != nil { - t.Fatal("Expected no error while decrypting session key, got:", err) - } - - if decryptedSessionKey.GetBase64Key() != sessionKey.GetBase64Key() { - t.Error("Decrypted session key is not equal to the original session key") - } -} - -func TestEncryptSignBinaryDetached(t *testing.T) { - plainData := []byte("Secret message") - privateKeyString := readTestFile("keyring_privateKey", false) - privateKey, err := crypto.NewKeyFromArmored(privateKeyString) - if err != nil { - t.Fatal("Error reading the test private key: ", err) - } - publicKeyString, err := privateKey.GetArmoredPublicKey() - if err != nil { - t.Fatal("Error reading the test public key: ", err) - } - encryptedData, armoredSignature, err := EncryptSignBinaryDetached( - publicKeyString, - privateKeyString, - testMailboxPassword, // Password defined in base_test - plainData, - ) - if err != nil { - t.Fatal("Expected no error while encrypting and signing, got:", err) - } - - decrypted, err := DecryptVerifyBinaryDetached( - publicKeyString, - privateKeyString, - testMailboxPassword, - encryptedData, - armoredSignature, - ) - - if err != nil { - t.Fatal("Expected no error while decrypting and verifying, got:", err) - } - - if !bytes.Equal(decrypted, plainData) { - t.Error("Decrypted is not equal to the plaintext") - } - - _, modifiedSignature, err := EncryptSignBinaryDetached( - publicKeyString, - privateKeyString, - testMailboxPassword, // Password defined in base_test - []byte("Different message"), - ) - - if err != nil { - t.Fatal("Expected no error while encrypting and signing, got:", err) - } - - _, err = DecryptVerifyBinaryDetached( - publicKeyString, - privateKeyString, - testMailboxPassword, - encryptedData, - modifiedSignature, - ) - - if err == nil { - t.Fatal("Expected an error while decrypting and verifying with a wrong signature") - } -} diff --git a/helper/key.go b/helper/key.go deleted file mode 100644 index 6352545b..00000000 --- a/helper/key.go +++ /dev/null @@ -1,63 +0,0 @@ -package helper - -import ( - "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/pkg/errors" -) - -// UpdatePrivateKeyPassphrase decrypts the given armored privateKey with oldPassphrase, -// re-encrypts it with newPassphrase, and returns the new armored key. -func UpdatePrivateKeyPassphrase( - privateKey string, - oldPassphrase, newPassphrase []byte, -) (string, error) { - key, err := crypto.NewKeyFromArmored(privateKey) - if err != nil { - return "", errors.Wrap(err, "gopenpgp: unable to parse key") - } - - unlocked, err := key.Unlock(oldPassphrase) - if err != nil { - return "", errors.Wrap(err, "gopenpgp: unable to unlock old key") - } - defer unlocked.ClearPrivateParams() - - locked, err := unlocked.Lock(newPassphrase) - if err != nil { - return "", errors.Wrap(err, "gopenpgp: unable to lock new key") - } - - armored, err := locked.Armor() - if err != nil { - return "", errors.Wrap(err, "gopenpgp: unable to armor new key") - } - - return armored, nil -} - -// GenerateKey generates a key of the given keyType ("rsa" or "x25519"), encrypts it, and returns an armored string. -// If keyType is "rsa", bits is the RSA bitsize of the key. -// If keyType is "x25519" bits is unused. -func GenerateKey(name, email string, passphrase []byte, keyType string, bits int) (string, error) { - key, err := crypto.GenerateKey(name, email, keyType, bits) - if err != nil { - return "", errors.Wrap(err, "gopenpgp: unable to generate new key") - } - defer key.ClearPrivateParams() - - locked, err := key.Lock(passphrase) - if err != nil { - return "", errors.Wrap(err, "gopenpgp: unable to lock new key") - } - - return locked.Armor() -} - -func GetSHA256Fingerprints(publicKey string) ([]string, error) { - key, err := crypto.NewKeyFromArmored(publicKey) - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: unable to parse key") - } - - return key.GetSHA256Fingerprints(), nil -} diff --git a/helper/key_test.go b/helper/key_test.go deleted file mode 100644 index 08d6986a..00000000 --- a/helper/key_test.go +++ /dev/null @@ -1,18 +0,0 @@ -package helper - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestGetSHA256FingerprintsV4(t *testing.T) { - sha256Fingerprints, err := GetSHA256Fingerprints(readTestFile("keyring_publicKey", false)) - if err != nil { - t.Fatal("Cannot unarmor key:", err) - } - - assert.Len(t, sha256Fingerprints, 2) - assert.Exactly(t, "d9ac0b857da6d2c8be985b251a9e3db31e7a1d2d832d1f07ebe838a9edce9c24", sha256Fingerprints[0]) - assert.Exactly(t, "203dfba1f8442c17e59214d9cd11985bfc5cc8721bb4a71740dd5507e58a1a0d", sha256Fingerprints[1]) -} diff --git a/helper/message.go b/helper/message.go deleted file mode 100644 index b4e58ff0..00000000 --- a/helper/message.go +++ /dev/null @@ -1,22 +0,0 @@ -package helper - -import "github.com/ProtonMail/gopenpgp/v2/crypto" - -// EncryptPGPMessageToAdditionalKey decrypts the session key of the input PGPSplitMessage with a private key in keyRing -// and encrypts it towards the additionalKeys by adding the additional key packets to the input PGPSplitMessage. -// If successful, new key packets are added to message. -// * messageToModify : The encrypted pgp message that should be modified -// * keyRing : The private keys to decrypt the session key in the messageToModify. -// * additionalKey : The public keys the message should be additionally encrypted to. -func EncryptPGPMessageToAdditionalKey(messageToModify *crypto.PGPSplitMessage, keyRing *crypto.KeyRing, additionalKey *crypto.KeyRing) error { - sessionKey, err := keyRing.DecryptSessionKey(messageToModify.KeyPacket) - if err != nil { - return err - } - additionalKeyPacket, err := additionalKey.EncryptSessionKey(sessionKey) - if err != nil { - return err - } - messageToModify.KeyPacket = append(messageToModify.KeyPacket, additionalKeyPacket...) - return nil -} diff --git a/helper/message_test.go b/helper/message_test.go deleted file mode 100644 index 9b140bff..00000000 --- a/helper/message_test.go +++ /dev/null @@ -1,56 +0,0 @@ -package helper_test - -import ( - "testing" - - "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/ProtonMail/gopenpgp/v2/helper" - "github.com/stretchr/testify/assert" -) - -func TestEncryptPGPMessageToAdditionalKey(t *testing.T) { - keyA, err := crypto.GenerateKey("A", "a@a.a", "x25519", 0) - if err != nil { - t.Fatal("Expected no error when generating key, got:", err) - } - - keyB, err := crypto.GenerateKey("B", "b@b.b", "x25519", 0) - if err != nil { - t.Fatal("Expected no error when generating key, got:", err) - } - - keyRingA, err := crypto.NewKeyRing(keyA) - if err != nil { - t.Fatal("Expected no error when creating keyring, got:", err) - } - keyRingB, err := crypto.NewKeyRing(keyB) - if err != nil { - t.Fatal("Expected no error when creating keyring, got:", err) - } - - message := crypto.NewPlainMessageFromString("plain text") - // Encrypt towards A - ciphertext, err := keyRingA.Encrypt(message, nil) - if err != nil { - t.Fatal("Expected no error when encrypting, got:", err) - } - ciphertextSplit, err := ciphertext.SplitMessage() - if err != nil { - t.Fatal("Expected no error when splitting message, got:", err) - } - // Also encrypt the message towards B - if err := helper.EncryptPGPMessageToAdditionalKey(ciphertextSplit, keyRingA, keyRingB); err != nil { - t.Fatal("Expected no error when modifying the message, got:", err) - } - - // Test decrypt with B - decrypted, err := keyRingB.Decrypt( - ciphertextSplit.GetPGPMessage(), - nil, - 0, - ) - if err != nil { - t.Fatal("Expected no error when decrypting, got:", err) - } - assert.Exactly(t, message.GetString(), decrypted.GetString()) -} diff --git a/helper/mobile.go b/helper/mobile.go deleted file mode 100644 index a08d81d9..00000000 --- a/helper/mobile.go +++ /dev/null @@ -1,178 +0,0 @@ -package helper - -import ( - "encoding/json" - goerrors "errors" - "runtime/debug" - - "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/pkg/errors" -) - -type ExplicitVerifyMessage struct { - Message *crypto.PlainMessage - SignatureVerificationError *crypto.SignatureVerificationError -} - -// DecryptExplicitVerify decrypts a PGP message given a private keyring -// and a public keyring to verify the embedded signature. Returns the plain -// data and an error on signature verification failure. -func DecryptExplicitVerify( - pgpMessage *crypto.PGPMessage, - privateKeyRing, publicKeyRing *crypto.KeyRing, - verifyTime int64, -) (*ExplicitVerifyMessage, error) { - message, err := privateKeyRing.Decrypt(pgpMessage, publicKeyRing, verifyTime) - return newExplicitVerifyMessage(message, err) -} - -// DecryptExplicitVerifyWithContext decrypts a PGP message given a private keyring -// and a public keyring to verify the embedded signature. Returns the plain -// data and an error on signature verification failure. -// The caller can provide a context that will be used to verify the signature. -func DecryptExplicitVerifyWithContext( - pgpMessage *crypto.PGPMessage, - privateKeyRing, publicKeyRing *crypto.KeyRing, - verifyTime int64, - verificationContext *crypto.VerificationContext, -) (*ExplicitVerifyMessage, error) { - message, err := privateKeyRing.DecryptWithContext(pgpMessage, publicKeyRing, verifyTime, verificationContext) - return newExplicitVerifyMessage(message, err) -} - -// DecryptSessionKeyExplicitVerify decrypts a PGP data packet given a session key -// and a public keyring to verify the embedded signature. Returns the plain data and -// an error on signature verification failure. -func DecryptSessionKeyExplicitVerify( - dataPacket []byte, - sessionKey *crypto.SessionKey, - publicKeyRing *crypto.KeyRing, - verifyTime int64, -) (*ExplicitVerifyMessage, error) { - message, err := sessionKey.DecryptAndVerify(dataPacket, publicKeyRing, verifyTime) - return newExplicitVerifyMessage(message, err) -} - -// DecryptSessionKeyExplicitVerifyWithContext decrypts a PGP data packet given a session key -// and a public keyring to verify the embedded signature. Returns the plain data and -// an error on signature verification failure. -// The caller can provide a context that will be used to verify the signature. -func DecryptSessionKeyExplicitVerifyWithContext( - dataPacket []byte, - sessionKey *crypto.SessionKey, - publicKeyRing *crypto.KeyRing, - verifyTime int64, - verificationContext *crypto.VerificationContext, -) (*ExplicitVerifyMessage, error) { - message, err := sessionKey.DecryptAndVerifyWithContext(dataPacket, publicKeyRing, verifyTime, verificationContext) - return newExplicitVerifyMessage(message, err) -} - -func newExplicitVerifyMessage(message *crypto.PlainMessage, err error) (*ExplicitVerifyMessage, error) { - var explicitVerify *ExplicitVerifyMessage - if err != nil { - castedErr := &crypto.SignatureVerificationError{} - isType := goerrors.As(err, castedErr) - if !isType { - return nil, errors.Wrap(err, "gopenpgp: unable to decrypt message") - } - - explicitVerify = &ExplicitVerifyMessage{ - Message: message, - SignatureVerificationError: castedErr, - } - } else { - explicitVerify = &ExplicitVerifyMessage{ - Message: message, - SignatureVerificationError: nil, - } - } - - return explicitVerify, nil -} - -// DecryptAttachment takes a keypacket and datpacket -// and returns a decrypted PlainMessage -// Specifically designed for attachments rather than text messages. -func DecryptAttachment(keyPacket []byte, dataPacket []byte, keyRing *crypto.KeyRing) (*crypto.PlainMessage, error) { - splitMessage := crypto.NewPGPSplitMessage(keyPacket, dataPacket) - - decrypted, err := keyRing.DecryptAttachment(splitMessage) - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: unable to decrypt attachment") - } - return decrypted, nil -} - -// EncryptAttachment encrypts a file given a plainData and a fileName. -// Returns a PGPSplitMessage containing a session key packet and symmetrically -// encrypted data. Specifically designed for attachments rather than text -// messages. -func EncryptAttachment(plainData []byte, filename string, keyRing *crypto.KeyRing) (*crypto.PGPSplitMessage, error) { - plainMessage := crypto.NewPlainMessageFromFile(plainData, filename, uint32(crypto.GetUnixTime())) - decrypted, err := keyRing.EncryptAttachment(plainMessage, "") - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: unable to encrypt attachment") - } - return decrypted, nil -} - -// GetJsonSHA256Fingerprints returns the SHA256 fingeprints of key and subkeys, -// encoded in JSON, since gomobile can not handle arrays. -func GetJsonSHA256Fingerprints(publicKey string) ([]byte, error) { - key, err := crypto.NewKeyFromArmored(publicKey) - if err != nil { - return nil, errors.Wrap(err, "gopenpgp: unable to parse key") - } - - return json.Marshal(key.GetSHA256Fingerprints()) -} - -type EncryptSignArmoredDetachedMobileResult struct { - CiphertextArmored, EncryptedSignatureArmored string -} - -// EncryptSignArmoredDetachedMobile wraps the encryptSignArmoredDetached method -// to have only one return argument for mobile. -func EncryptSignArmoredDetachedMobile( - publicKey, privateKey string, - passphrase, plainData []byte, -) (wrappedTuple *EncryptSignArmoredDetachedMobileResult, err error) { - ciphertext, encryptedSignature, err := encryptSignArmoredDetached(publicKey, privateKey, passphrase, plainData) - if err != nil { - return nil, err - } - - return &EncryptSignArmoredDetachedMobileResult{ - CiphertextArmored: ciphertext, - EncryptedSignatureArmored: encryptedSignature, - }, nil -} - -type EncryptSignBinaryDetachedMobileResult struct { - EncryptedData []byte - EncryptedSignatureArmored string -} - -// EncryptSignBinaryDetachedMobile wraps the encryptSignBinaryDetached method -// to have only one return argument for mobile. -func EncryptSignBinaryDetachedMobile( - publicKey, privateKey string, - passphrase, plainData []byte, -) (wrappedTuple *EncryptSignBinaryDetachedMobileResult, err error) { - ciphertext, encryptedSignature, err := encryptSignBinaryDetached(publicKey, privateKey, passphrase, plainData) - if err != nil { - return nil, err - } - return &EncryptSignBinaryDetachedMobileResult{ - EncryptedData: ciphertext, - EncryptedSignatureArmored: encryptedSignature, - }, nil -} - -// FreeOSMemory can be used to explicitly -// call the garbage collector and -// return the unused memory to the OS. -func FreeOSMemory() { - debug.FreeOSMemory() -} diff --git a/helper/mobile_test.go b/helper/mobile_test.go deleted file mode 100644 index 54751b2f..00000000 --- a/helper/mobile_test.go +++ /dev/null @@ -1,115 +0,0 @@ -package helper - -import ( - "testing" - - "github.com/ProtonMail/gopenpgp/v2/constants" - "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/stretchr/testify/assert" -) - -func TestMobileSignedMessageDecryption(t *testing.T) { - privateKey, _ := crypto.NewKeyFromArmored(readTestFile("keyring_privateKey", false)) - // Password defined in base_test - privateKey, err := privateKey.Unlock(testMailboxPassword) - if err != nil { - t.Fatal("Expected no error unlocking privateKey, got:", err) - } - testPrivateKeyRing, _ := crypto.NewKeyRing(privateKey) - - publicKey, _ := crypto.NewKeyFromArmored(readTestFile("mime_publicKey", false)) - testPublicKeyRing, _ := crypto.NewKeyRing(publicKey) - - pgpMessage, err := crypto.NewPGPMessageFromArmored(readTestFile("message_signed", false)) - if err != nil { - t.Fatal("Expected no error when unarmoring, got:", err) - } - - decrypted, err := DecryptExplicitVerify(pgpMessage, testPrivateKeyRing, testPublicKeyRing, crypto.GetUnixTime()) - if err != nil { - t.Fatal("Expected no error when decrypting, got:", err) - } - - assert.Exactly(t, constants.SIGNATURE_NO_VERIFIER, decrypted.SignatureVerificationError.Status) - assert.Exactly(t, readTestFile("message_plaintext", true), decrypted.Message.GetString()) - - publicKey, _ = crypto.NewKeyFromArmored(readTestFile("keyring_publicKey", false)) - testPublicKeyRing, _ = crypto.NewKeyRing(publicKey) - - pgpMessage, err = testPublicKeyRing.Encrypt(decrypted.Message, testPrivateKeyRing) - if err != nil { - t.Fatal("Expected no error when encrypting, got:", err) - } - - decrypted, err = DecryptExplicitVerify(pgpMessage, testPrivateKeyRing, testPublicKeyRing, crypto.GetUnixTime()) - if err != nil { - t.Fatal("Expected no error when decrypting, got:", err) - } - - assert.Nil(t, decrypted.SignatureVerificationError) - assert.Exactly(t, readTestFile("message_plaintext", true), decrypted.Message.GetString()) - - decrypted, err = DecryptExplicitVerify(pgpMessage, testPublicKeyRing, testPublicKeyRing, crypto.GetUnixTime()) - assert.NotNil(t, err) - assert.Nil(t, decrypted) -} - -func TestMobileSignedMessageDecryptionWithSessionKey(t *testing.T) { - var message = crypto.NewPlainMessageFromString( - "The secret code is... 1, 2, 3, 4, 5. I repeat: the secret code is... 1, 2, 3, 4, 5", - ) - - privateKey, _ := crypto.NewKeyFromArmored(readTestFile("keyring_privateKey", false)) - // Password defined in base_test - privateKey, err := privateKey.Unlock(testMailboxPassword) - if err != nil { - t.Fatal("Expected no error unlocking privateKey, got:", err) - } - testPrivateKeyRing, _ := crypto.NewKeyRing(privateKey) - - publicKey, _ := crypto.NewKeyFromArmored(readTestFile("keyring_publicKey", false)) - testPublicKeyRing, _ := crypto.NewKeyRing(publicKey) - - sk, err := crypto.GenerateSessionKey() - if err != nil { - t.Fatal("Expected no error generating session key, got:", err) - } - - pgpMessage, err := sk.Encrypt(message) - if err != nil { - t.Fatal("Expected no error when unarmoring, got:", err) - } - - decrypted, err := DecryptSessionKeyExplicitVerify(pgpMessage, sk, testPublicKeyRing, crypto.GetUnixTime()) - if err != nil { - t.Fatal("Expected no error when decrypting, got:", err) - } - - assert.Exactly(t, constants.SIGNATURE_NOT_SIGNED, decrypted.SignatureVerificationError.Status) - assert.Exactly(t, message.GetString(), decrypted.Message.GetString()) - - publicKey, _ = crypto.NewKeyFromArmored(readTestFile("keyring_publicKey", false)) - testPublicKeyRing, _ = crypto.NewKeyRing(publicKey) - - pgpMessage, err = sk.EncryptAndSign(message, testPrivateKeyRing) - if err != nil { - t.Fatal("Expected no error when encrypting, got:", err) - } - - decrypted, err = DecryptSessionKeyExplicitVerify(pgpMessage, sk, testPublicKeyRing, crypto.GetUnixTime()) - if err != nil { - t.Fatal("Expected no error when decrypting, got:", err) - } - - assert.Nil(t, decrypted.SignatureVerificationError) - assert.Exactly(t, message.GetString(), decrypted.Message.GetString()) -} - -func TestGetJsonSHA256FingerprintsV4(t *testing.T) { - sha256Fingerprints, err := GetJsonSHA256Fingerprints(readTestFile("keyring_publicKey", false)) - if err != nil { - t.Fatal("Cannot unarmor key:", err) - } - - assert.Exactly(t, []byte("[\"d9ac0b857da6d2c8be985b251a9e3db31e7a1d2d832d1f07ebe838a9edce9c24\",\"203dfba1f8442c17e59214d9cd11985bfc5cc8721bb4a71740dd5507e58a1a0d\"]"), sha256Fingerprints) -} diff --git a/helper/sign_detached.go b/helper/sign_detached.go deleted file mode 100644 index 76c8ff9a..00000000 --- a/helper/sign_detached.go +++ /dev/null @@ -1,70 +0,0 @@ -//go:build !ios && !android -// +build !ios,!android - -package helper - -import ( - "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/pkg/errors" -) - -// EncryptSignAttachment encrypts an attachment using a detached signature, given a publicKey, a privateKey -// and its passphrase, the filename, and the unencrypted file data. -// Returns keypacket, dataPacket and unarmored (!) signature separate. -func EncryptSignAttachment( - publicKey, privateKey string, passphrase []byte, filename string, plainData []byte, -) (keyPacket, dataPacket, signature []byte, err error) { - var privateKeyObj, unlockedKeyObj *crypto.Key - var publicKeyRing, privateKeyRing *crypto.KeyRing - var packets *crypto.PGPSplitMessage - var signatureObj *crypto.PGPSignature - - var binMessage = crypto.NewPlainMessageFromFile(plainData, filename, uint32(crypto.GetUnixTime())) - - if publicKeyRing, err = createPublicKeyRing(publicKey); err != nil { - return nil, nil, nil, err - } - - if privateKeyObj, err = crypto.NewKeyFromArmored(privateKey); err != nil { - return nil, nil, nil, errors.Wrap(err, "gopenpgp: unable to parse private key") - } - - if unlockedKeyObj, err = privateKeyObj.Unlock(passphrase); err != nil { - return nil, nil, nil, errors.Wrap(err, "gopenpgp: unable to unlock key") - } - defer unlockedKeyObj.ClearPrivateParams() - - if privateKeyRing, err = crypto.NewKeyRing(unlockedKeyObj); err != nil { - return nil, nil, nil, errors.Wrap(err, "gopenpgp: unable to create private keyring") - } - - if packets, err = publicKeyRing.EncryptAttachment(binMessage, ""); err != nil { - return nil, nil, nil, errors.Wrap(err, "gopenpgp: unable to encrypt attachment") - } - - if signatureObj, err = privateKeyRing.SignDetached(binMessage); err != nil { - return nil, nil, nil, errors.Wrap(err, "gopenpgp: unable to sign attachment") - } - - return packets.GetBinaryKeyPacket(), packets.GetBinaryDataPacket(), signatureObj.GetBinary(), nil -} - -// EncryptSignArmoredDetached takes a public key for encryption, -// a private key and its passphrase for signature, and the plaintext data -// Returns an armored ciphertext and a detached armored signature. -func EncryptSignArmoredDetached( - publicKey, privateKey string, - passphrase, plainData []byte, -) (ciphertextArmored, encryptedSignatureArmored string, err error) { - return encryptSignArmoredDetached(publicKey, privateKey, passphrase, plainData) -} - -// EncryptSignBinaryDetached takes a public key for encryption, -// a private key and its passphrase for signature, and the plaintext data -// Returns encrypted binary data and a detached armored encrypted signature. -func EncryptSignBinaryDetached( - publicKey, privateKey string, - passphrase, plainData []byte, -) (encryptedData []byte, encryptedSignatureArmored string, err error) { - return encryptSignBinaryDetached(publicKey, privateKey, passphrase, plainData) -} diff --git a/internal/armor.go b/internal/armor.go index 0cf8f7db..405a0060 100644 --- a/internal/armor.go +++ b/internal/armor.go @@ -1,6 +1,7 @@ package internal import ( + "bytes" "strings" "github.com/ProtonMail/go-crypto/openpgp/armor" @@ -16,3 +17,13 @@ func Unarmor(input string) (*armor.Block, error) { } return b, nil } + +// UnarmorBytes unarmors an armored byte slice. +func UnarmorBytes(input []byte) (*armor.Block, error) { + io := bytes.NewReader(input) + b, err := armor.Decode(io) + if err != nil { + return nil, errors.Wrap(err, "gopenpgp: unable to unarmor") + } + return b, nil +} diff --git a/internal/common.go b/internal/common.go index 534b33d2..588b956f 100644 --- a/internal/common.go +++ b/internal/common.go @@ -2,15 +2,25 @@ package internal import ( + "bytes" + "errors" + "io" "strings" - "github.com/ProtonMail/gopenpgp/v2/constants" + "github.com/ProtonMail/gopenpgp/v3/constants" ) +var nl []byte = []byte("\n") +var rnl []byte = []byte("\r\n") + func Canonicalize(text string) string { return strings.ReplaceAll(strings.ReplaceAll(text, "\r\n", "\n"), "\n", "\r\n") } +func CanonicalizeBytes(text []byte) []byte { + return bytes.ReplaceAll(bytes.ReplaceAll(text, rnl, nl), nl, rnl) +} + func TrimEachLine(text string) string { lines := strings.Split(text, "\n") @@ -21,12 +31,84 @@ func TrimEachLine(text string) string { return strings.Join(lines, "\n") } -// CreationTimeOffset stores the amount of seconds that a signature may be -// created in the future, to compensate for clock skew. -const CreationTimeOffset = int64(60 * 60 * 24 * 2) +func TrimEachLineBytes(text []byte) []byte { + lines := bytes.Split(text, nl) + + for i := range lines { + lines[i] = bytes.TrimRight(lines[i], " \t\r") + } + + return bytes.Join(lines, nl) +} // ArmorHeaders is a map of default armor headers. -var ArmorHeaders = map[string]string{ - "Version": constants.ArmorHeaderVersion, - "Comment": constants.ArmorHeaderComment, +var ArmorHeaders = map[string]string{} + +func init() { + if constants.ArmorHeaderEnabled { + ArmorHeaders = map[string]string{ + "Version": constants.ArmorHeaderVersion, + "Comment": constants.ArmorHeaderComment, + } + } +} + +// ResetReader is a reader that can be reset by buffering data internally. +type ResetReader struct { + Reader io.Reader + buffer *bytes.Buffer + bufferData bool +} + +// NewResetReader creates a new ResetReader with the default state. +func NewResetReader(reader io.Reader) *ResetReader { + return &ResetReader{ + Reader: reader, + buffer: bytes.NewBuffer(nil), + bufferData: true, + } +} + +func (rr *ResetReader) Read(b []byte) (n int, err error) { + n, err = rr.Reader.Read(b) + if rr.bufferData { + rr.buffer.Write(b[:n]) + } + return +} + +// DisableBuffering disables the internal buffering. +// After the disable, a Reset is not allowed anymore. +func (rr *ResetReader) DisableBuffering() { + rr.bufferData = false +} + +// Reset creates a reader that reads again from the beginning and +// resets the internal state. +func (rr *ResetReader) Reset() (io.Reader, error) { + if !rr.bufferData { + return nil, errors.New("reset not possible if buffering is disabled") + } + rr.Reader = io.MultiReader(rr.buffer, rr.Reader) + rr.buffer = bytes.NewBuffer(nil) + return rr.Reader, nil +} + +type noOpWriteCloser struct { + writer io.Writer +} + +// NewNoOpWriteCloser creates a WriteCloser form a Writer that performs no operation on close. +func NewNoOpWriteCloser(writer io.Writer) io.WriteCloser { + return &noOpWriteCloser{ + writer, + } +} + +func (w *noOpWriteCloser) Write(p []byte) (n int, err error) { + return w.writer.Write(p) +} + +func (w *noOpWriteCloser) Close() error { + return nil } diff --git a/internal/sanitize_string.go b/internal/sanitize_string.go new file mode 100644 index 00000000..6ee30d3c --- /dev/null +++ b/internal/sanitize_string.go @@ -0,0 +1,136 @@ +package internal + +import ( + "bufio" + "bytes" + "io" + "strings" + "unicode" + "unicode/utf8" +) + +func SanitizeString(input string) string { + return strings.ToValidUTF8(input, string(unicode.ReplacementChar)) +} + +func NewSanitizeReader(r io.Reader) io.Reader { + sanitizer := &sanitizeReader{r, new(bytes.Buffer), false} + return newSanitizeUtf8Reader(sanitizer) +} + +type sanitizeUtf8Reader struct { + r *bufio.Reader + reminder []byte + internalBuffer [4]byte + lastRuneInvalid bool +} + +func newSanitizeUtf8Reader(reader io.Reader) *sanitizeUtf8Reader { + return &sanitizeUtf8Reader{ + r: bufio.NewReader(reader), + } +} + +func (sr *sanitizeUtf8Reader) Read(buf []byte) (int, error) { + read := 0 + // Check if there is a reminder from the previous read + if sr.reminder != nil { + toCopy := len(sr.reminder) + if toCopy > len(buf) { + toCopy = len(buf) + } + copy(buf[read:], sr.reminder[:toCopy]) + read += toCopy + if toCopy < len(sr.reminder) { + sr.reminder = sr.reminder[toCopy:] + } else { + sr.reminder = nil + } + } + // Decode utf-8 runes from the internal reader and copy + for read < len(buf) { + runeItem, size, err := sr.r.ReadRune() + if err != nil { + return read, err + } + if runeItem == unicode.ReplacementChar { + // If last rune written is a replacement skip + if sr.lastRuneInvalid { + continue + } + size = 3 + sr.lastRuneInvalid = true + } else { + sr.lastRuneInvalid = false + } + if read+size <= len(buf) { + utf8.EncodeRune(buf[read:], runeItem) + read += size + } else { + // Not enough space to write the entire rune + size = utf8.EncodeRune(sr.internalBuffer[:], runeItem) + copied := copy(buf[read:], sr.internalBuffer[:len(buf)-read]) + sr.reminder = sr.internalBuffer[copied:size] + read += copied + break + } + } + return read, nil +} + +type sanitizeReader struct { + r io.Reader + buffer *bytes.Buffer + pin bool +} + +func (sr *sanitizeReader) resetState() { + sr.pin = false +} + +func (sr *sanitizeReader) Read(buf []byte) (int, error) { + // read from internal buffer first + internalRead, _ := sr.buffer.Read(buf) + if internalRead == len(buf) { + return internalRead, nil + } + // if there is more space in buf, read from the reader + n, err := sr.r.Read(buf[internalRead:]) + if err != nil && err != io.EOF { + // error occurred that is not EOF + return n, err + } + // filter non-unicode and \r\n in what has been read from the reader, + for i := internalRead; i < internalRead+n; { + c := buf[i] + if sr.pin { + // last char read is \r + if c == '\n' { + sr.buffer.WriteByte('\n') + i++ + } else { + sr.buffer.WriteByte('\r') + } + sr.resetState() + continue + } + if c == '\r' { + // check for \n on next char + i++ + sr.pin = true + continue + } + sr.resetState() + sr.buffer.Write(buf[i : i+1]) + i++ + } + if err == io.EOF && sr.pin { + sr.resetState() + sr.buffer.WriteByte('\r') + } + finalRead, _ := sr.buffer.Read(buf[internalRead:]) + if err == io.EOF && sr.buffer.Len() == 0 { + return internalRead + finalRead, err + } + return internalRead + finalRead, nil +} diff --git a/internal/sanitize_string_test.go b/internal/sanitize_string_test.go new file mode 100644 index 00000000..981272b3 --- /dev/null +++ b/internal/sanitize_string_test.go @@ -0,0 +1,37 @@ +package internal + +import ( + "bytes" + "io" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func expectedOutput(in string) string { + return SanitizeString(strings.ReplaceAll(in, "\r\n", "\n")) +} + +func testStringSanitizeReader(t *testing.T, test string) { + reader := NewSanitizeReader(bytes.NewReader([]byte(test))) + byteBuffer := new(bytes.Buffer) + smallBuff := make([]byte, 3) + var err error + for err != io.EOF { + var n int + n, err = reader.Read(smallBuff) + byteBuffer.Write(smallBuff[:n]) + } + assert.Equal(t, byteBuffer.String(), expectedOutput(test)) +} + +func TestStringSanitizeReader(t *testing.T) { + test := "a\xc5zsd\xc5\r\ndf\rdf\xc5df\rsdf\r\n\r\n\r\n\r\n\r\n\r\n\r\n" + testStringSanitizeReader(t, test) + testStringSanitizeReader(t, "\r\n\r\n\r\n\r\n\r\n\r\n\r\n") + testStringSanitizeReader(t, "\n") + testStringSanitizeReader(t, "\r") + testStringSanitizeReader(t, "") + testStringSanitizeReader(t, "\xc5\xc5\xc5\xc5\xc5\xc5\xc5\xc5\xc5") +} diff --git a/internal/testdata/utf8Valid.txt b/internal/testdata/utf8Valid.txt new file mode 100644 index 00000000..55190b9d --- /dev/null +++ b/internal/testdata/utf8Valid.txt @@ -0,0 +1,29 @@ +In the realm of encryption, where secrets weave their tale, +OpenPGP emerges, a guardian in the digital vale. +Keys, like whispers, dance in cryptographic delight, +A symphony of security, in the quiet of the ๐“ƒ๐’พ๐‘”๐’ฝ๐“‰. + +Public and private, a ๐•œ๐•–๐•ช ๐•ก๐•’๐•š๐•ฃ takes the stage, +A ballet of ๐–‡๐–Ž๐–™๐–˜, in a secure encryption ๐•”๐•’๐•˜๐•–. +๐“๐“ต๐“ฐ๐“ธ๐“ป๐“ฒ๐“ฝ๐“ฑ๐“ถ๐“ผ ๐”€๐“ช๐“ต๐“ฝ๐”ƒ, a choreography so divine, +OpenPGP orchestrates, where secrets ๐’พ๐“ƒ๐“‰๐‘’๐“‡๐“Œ๐“ฒ๐“ƒ๐“ฎ. + +Binary ballet, a dance of ones and zeros, +In the realm of cyberspace, where ๐“น๐“ป๐“ฒ๐“ฟ๐“ช๐“ฌ๐”‚ bestows. +Messages wrapped in ciphers, like a ๐“น๐“ธ๐“ฎ๐“ฝ๐“ฒ๐“ฌ ๐“ช๐“ป๐“ฝ, +OpenPGP's embrace, a shield around the ๐“ฑ๐“ฎ๐“ช๐“ป๐“ฝ. + +In the ๐“ต๐“ช๐“ท๐“ฐ๐“พ๐“ช๐“ฐ๐“ฎ of encryption, trust is finely spun, +A shield for emails, where shadows cannot ๐“ผ๐“ฑ๐“พ๐“ท. +Securely enveloped, like letters sealed with wax, +OpenPGP guards the ๐•˜๐•’๐•ฅ๐•–๐•ค, against unwarranted ๐•’๐•ฅ๐•ฅ๐•’๐•”๐•œ๐•ค. + +Through the ๐•ง๐•’๐•ค๐•ฅ ๐•–๐•ฉ๐•ก๐•’๐•Ÿ๐•ค๐•– of the digital ๐•ค๐•–๐•’, +OpenPGP sails, setting secrets ๐“ฏ๐“ป๐“ฎ๐“ฎ. +A cryptographic journey, in a code-bound ๐•ค๐•™๐•š๐•ก, +Navigating the currents, on a secure ๐•—๐•–๐•๐•๐• ๐•จ๐•ค๐•™๐•š๐•ก. + +So here's to OpenPGP, the ๐“น๐“ธ๐“ฎ๐“ฝ of encryption's ๐“ต๐“ธ๐“ป๐“ฎ, +A guardian of ๐“น๐“ป๐“ฒ๐“ฟ๐“ช๐“ฌ๐”‚, on the digital ๐•ค๐•™๐• ๐•ฃ๐•–. +In the language of ๐•’๐•๐•˜๐• ๐•ฃ๐•š๐•ฅ๐•™๐•ž๐•ค, where trust is ๐••๐•–๐•–๐•ก๐•๐•ช ๐•ค๐• ๐•จ๐•Ÿ, +OpenPGP, a ๐“น๐“ธ๐“ฎ๐“ถ in ๐–ˆ๐–”๐–‰๐–Š, forever shall be known. \ No newline at end of file diff --git a/internal/utf8.go b/internal/utf8.go new file mode 100644 index 00000000..1802e359 --- /dev/null +++ b/internal/utf8.go @@ -0,0 +1,182 @@ +package internal + +import ( + "errors" + "io" + "unicode/utf8" +) + +var ErrIncorrectUtf8 = errors.New("openpgp: data encoding is not valid utf-8") + +const ( + maxRuneSize = 4 + + locb = 0b10000000 + hicb = 0b10111111 + + xx = 0xF1 + as = 0xF0 + s1 = 0x02 + s2 = 0x13 + s3 = 0x03 + s4 = 0x23 + s5 = 0x34 + s6 = 0x04 + s7 = 0x44 +) + +var first = [256]uint8{ + // 1 2 3 4 5 6 7 8 9 A B C D E F + as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x00-0x0F + as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x10-0x1F + as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x20-0x2F + as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x30-0x3F + as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x40-0x4F + as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x50-0x5F + as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x60-0x6F + as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x70-0x7F + // 1 2 3 4 5 6 7 8 9 A B C D E F + xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, // 0x80-0x8F + xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, // 0x90-0x9F + xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, // 0xA0-0xAF + xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, // 0xB0-0xBF + xx, xx, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, // 0xC0-0xCF + s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, // 0xD0-0xDF + s2, s3, s3, s3, s3, s3, s3, s3, s3, s3, s3, s3, s3, s4, s3, s3, // 0xE0-0xEF + s5, s6, s6, s6, s7, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, // 0xF0-0xFF +} + +type acceptRange struct { + lo uint8 + hi uint8 +} + +var acceptRanges = [16]acceptRange{ + 0: {locb, hicb}, + 1: {0xA0, hicb}, + 2: {locb, 0x9F}, + 3: {0x90, hicb}, + 4: {locb, 0x8F}, +} + +func canOverlap(in []byte) []byte { + if len(in) < maxRuneSize { + return in + } + return nil +} + +// valid is a slightly modified utf8.Valid() function copied from the standard library. +// If the byte slice is not valid utf8, it additionally returns the remaining data if the +// remaining data is smaller than the largest potential rune. +func valid(p []byte) (bool, []byte) { + p = p[:len(p):len(p)] + for len(p) >= 8 { + first32 := uint32(p[0]) | uint32(p[1])<<8 | uint32(p[2])<<16 | uint32(p[3])<<24 + second32 := uint32(p[4]) | uint32(p[5])<<8 | uint32(p[6])<<16 | uint32(p[7])<<24 + if (first32|second32)&0x80808080 != 0 { + break + } + p = p[8:] + } + n := len(p) + for i := 0; i < n; { + pi := p[i] + if pi < utf8.RuneSelf { + i++ + continue + } + x := first[pi] + if x == xx { + return false, canOverlap(p[i:]) + } + size := int(x & 7) + if i+size > n { + return false, canOverlap(p[i:]) + } + accept := acceptRanges[x>>4] + if c := p[i+1]; c < accept.lo || accept.hi < c { + return false, canOverlap(p[i:]) + } else if size == 2 { + } else if c := p[i+2]; c < locb || hicb < c { + return false, canOverlap(p[i:]) + } else if size == 3 { + } else if c := p[i+3]; c < locb || hicb < c { + return false, canOverlap(p[i:]) + } + i += size + } + return true, nil +} + +type utf8Checker struct { + buffer [maxRuneSize]byte + overflowSize int +} + +func (c *utf8Checker) check(p []byte) error { + pInspect := p + if c.overflowSize > 0 { + // There is data in the overflow buffer from the last check call + copied := copy(c.buffer[c.overflowSize:], p) + c.overflowSize += copied + r, runeSize := utf8.DecodeRune(c.buffer[:c.overflowSize]) + if r == utf8.RuneError && runeSize == 1 { + if c.overflowSize < maxRuneSize { + // Could still be valid utf-8 on next check + return nil + } + return ErrIncorrectUtf8 + } + pInspect = pInspect[copied-(c.overflowSize-runeSize):] + c.overflowSize = 0 + } + if len(pInspect) < 1 { + return nil + } + isValid, rest := valid(pInspect) + if !isValid && len(rest) > 0 { + // Could still be valid utf-8 on next check + copy(c.buffer[:], rest) + c.overflowSize = len(rest) + } else if !isValid { + return ErrIncorrectUtf8 + } + return nil +} + +func (c *utf8Checker) close() error { + if c.overflowSize > 0 { + return ErrIncorrectUtf8 + } + return nil +} + +type Utf8CheckWriteCloser struct { + utf8Checker + internal io.WriteCloser +} + +func NewUtf8CheckWriteCloser(wrap io.WriteCloser) *Utf8CheckWriteCloser { + return &Utf8CheckWriteCloser{ + internal: wrap, + } +} + +func (cw *Utf8CheckWriteCloser) Write(p []byte) (n int, err error) { + err = cw.check(p) + if err != nil { + return + } + n, err = cw.internal.Write(p) + return +} + +func (cw *Utf8CheckWriteCloser) Close() (err error) { + err = cw.close() + if err != nil { + return + } + err = cw.internal.Close() + return +} diff --git a/internal/utf8_test.go b/internal/utf8_test.go new file mode 100644 index 00000000..9b54d30a --- /dev/null +++ b/internal/utf8_test.go @@ -0,0 +1,78 @@ +package internal + +import ( + "bytes" + "encoding/hex" + "errors" + "io" + "os" + "strings" + "testing" +) + +var invalidUtf8 = []string{"f0288cbc", "fc80808080af"} + +var validUtf8 = []string{"HellโŒ˜oโ˜", "World", "||||", "ไฝ ๅฅฝ๏ผŒไธ–็•Œ๏ผ"} + +type noOpCloser struct { + buff *bytes.Buffer +} + +func (c *noOpCloser) Write(p []byte) (n int, err error) { + return c.buff.Write(p) +} + +func (c *noOpCloser) Close() (err error) { + return +} + +func loadLargeData(t *testing.T) { + data, err := os.ReadFile("testdata/utf8Valid.txt") + if err != nil { + t.Fatal(err) + } + validUtf8 = append(validUtf8, string(data)) +} + +func TestUtf8CheckWriteCloser(t *testing.T) { + loadLargeData(t) + t.Run("invalid utf-8", func(t *testing.T) { + for _, invalid := range invalidUtf8 { + buff := bytes.NewBuffer(nil) + writeCloser := NewUtf8CheckWriteCloser(&noOpCloser{buff}) + data, _ := hex.DecodeString(invalid) + var err error + for id := range data { + if _, err = writeCloser.Write(data[id : id+1]); err != nil { + break + } + } + errClose := writeCloser.Close() + if err == nil && errClose == nil { + t.Error("Should be invalid utf8") + } + } + }) + + t.Run("valid utf-8", func(t *testing.T) { + for _, copySize := range []int64{1, 3, 7, 11} { + for _, valid := range validUtf8 { + buff := bytes.NewBuffer(nil) + writeCloser := NewUtf8CheckWriteCloser(&noOpCloser{buff}) + dataReader := strings.NewReader(valid) + for { + _, err := io.CopyN(writeCloser, dataReader, copySize) + if errors.Is(err, io.EOF) { + break + } + if err != nil { + t.Fatal(err) + } + } + if err := writeCloser.Close(); err != nil { + t.Error("Should be valid utf8") + } + } + } + }) +} diff --git a/crypto/mime.go b/mime/mime.go similarity index 55% rename from crypto/mime.go rename to mime/mime.go index d756dfcc..a497a15f 100644 --- a/crypto/mime.go +++ b/mime/mime.go @@ -1,16 +1,16 @@ -package crypto +// Package mime provides an API to decrypt mime messages. +package mime import ( "bytes" - "io/ioutil" + "io" "net/mail" "net/textproto" - "strings" - "github.com/ProtonMail/go-crypto/openpgp" - "github.com/ProtonMail/go-crypto/openpgp/packet" gomime "github.com/ProtonMail/go-mime" - "github.com/ProtonMail/gopenpgp/v2/constants" + "github.com/ProtonMail/gopenpgp/v3/constants" + "github.com/ProtonMail/gopenpgp/v3/crypto" + "github.com/ProtonMail/gopenpgp/v3/internal" "github.com/pkg/errors" ) @@ -24,17 +24,27 @@ type MIMECallbacks interface { OnError(err error) } -// DecryptMIMEMessage decrypts a MIME message. -func (keyRing *KeyRing) DecryptMIMEMessage( - message *PGPMessage, verifyKey *KeyRing, callbacks MIMECallbacks, verifyTime int64, +// Decrypt decrypts and verifies a MIME message. +// messageEncoding provides the encoding of the encrypted MIME message, either crypto.Bytes or crypto.Armor. +// The decryptionHandle is used to decrypt and verify the message, while +// the verifyHandle is used to verify the signature contained in the decrypted mime message. +// The verifyHandle can be nil. +func Decrypt( + message []byte, + messageEncoding int8, // crypto.Bytes or crypto.Armor + decryptionHandle crypto.PGPDecryption, + verifyHandle crypto.PGPVerify, + callbacks MIMECallbacks, ) { - decryptedMessage, err := keyRing.Decrypt(message, verifyKey, verifyTime) - embeddedSigError, err := separateSigError(err) + decResult, err := decryptionHandle.Decrypt(message, messageEncoding) if err != nil { callbacks.OnError(err) return } - body, attachments, attachmentHeaders, err := parseMIME(string(decryptedMessage.GetBinary()), verifyKey) + decryptedMessage := decResult.Bytes() + embeddedSigError, _ := separateSigError(decResult.SignatureError()) + + body, attachments, attachmentHeaders, err := parseMIME(decryptedMessage, verifyHandle) mimeSigError, err := separateSigError(err) if err != nil { callbacks.OnError(err) @@ -45,11 +55,11 @@ func (keyRing *KeyRing) DecryptMIMEMessage( callbacks.OnError(embeddedSigError) callbacks.OnError(mimeSigError) callbacks.OnVerified(prioritizeSignatureErrors(embeddedSigError, mimeSigError)) - } else if verifyKey != nil { + } else if verifyHandle != nil { callbacks.OnVerified(constants.SIGNATURE_OK) } bodyContent, bodyMimeType := body.GetBody() - bodyContentSanitized := sanitizeString(bodyContent) + bodyContentSanitized := internal.SanitizeString(bodyContent) callbacks.OnBody(bodyContentSanitized, bodyMimeType) for i := 0; i < len(attachments); i++ { callbacks.OnAttachment(attachmentHeaders[i], []byte(attachments[i])) @@ -59,7 +69,7 @@ func (keyRing *KeyRing) DecryptMIMEMessage( // ----- INTERNAL FUNCTIONS ----- -func prioritizeSignatureErrors(signatureErrs ...*SignatureVerificationError) (maxError int) { +func prioritizeSignatureErrors(signatureErrs ...*crypto.SignatureVerificationError) (maxError int) { // select error with the highest value, if any // FAILED > NO VERIFIER > NOT SIGNED > SIGNATURE OK maxError = constants.SIGNATURE_OK @@ -71,8 +81,8 @@ func prioritizeSignatureErrors(signatureErrs ...*SignatureVerificationError) (ma return } -func separateSigError(err error) (*SignatureVerificationError, error) { - sigErr := &SignatureVerificationError{} +func separateSigError(err error) (*crypto.SignatureVerificationError, error) { + sigErr := &crypto.SignatureVerificationError{} if errors.As(err, sigErr) { return sigErr, nil } @@ -80,18 +90,18 @@ func separateSigError(err error) (*SignatureVerificationError, error) { } func parseMIME( - mimeBody string, verifierKey *KeyRing, + mimeBody []byte, + verifyHandle crypto.PGPVerify, ) (*gomime.BodyCollector, []string, []string, error) { - mm, err := mail.ReadMessage(strings.NewReader(mimeBody)) + mm, err := mail.ReadMessage(bytes.NewReader(mimeBody)) if err != nil { - return nil, nil, nil, errors.Wrap(err, "gopenpgp: error in reading message") + return nil, nil, nil, errors.Wrap(err, "mime: error in reading message") } - config := &packet.Config{DefaultCipher: packet.CipherAES256, Time: getTimeGenerator()} h := textproto.MIMEHeader(mm.Header) - mmBodyData, err := ioutil.ReadAll(mm.Body) + mmBodyData, err := io.ReadAll(mm.Body) if err != nil { - return nil, nil, nil, errors.Wrap(err, "gopenpgp: error in reading message body data") + return nil, nil, nil, errors.Wrap(err, "mime: error in reading message body data") } printAccepter := gomime.NewMIMEPrinter() @@ -99,15 +109,10 @@ func parseMIME( attachmentsCollector := gomime.NewAttachmentsCollector(bodyCollector) mimeVisitor := gomime.NewMimeVisitor(attachmentsCollector) - var verifierEntities openpgp.KeyRing - if verifierKey != nil { - verifierEntities = verifierKey.entities - } - - signatureCollector := newSignatureCollector(mimeVisitor, verifierEntities, config) + signatureCollector := newSignatureCollector(mimeVisitor, verifyHandle) err = gomime.VisitAll(bytes.NewReader(mmBodyData), h, signatureCollector) - if err == nil && verifierKey != nil { + if err == nil && verifyHandle != nil { err = signatureCollector.verified } diff --git a/crypto/mime_test.go b/mime/mime_test.go similarity index 74% rename from crypto/mime_test.go rename to mime/mime_test.go index a7f11926..3653a6b8 100644 --- a/crypto/mime_test.go +++ b/mime/mime_test.go @@ -1,11 +1,13 @@ -package crypto +package mime import ( "errors" - "io/ioutil" + "os" "path/filepath" + "strings" "testing" + "github.com/ProtonMail/gopenpgp/v3/crypto" "github.com/stretchr/testify/assert" ) @@ -40,7 +42,7 @@ func TestDecrypt(t *testing.T) { Testing: t, } - privateKey, err := NewKeyFromArmored(readTestFile("mime_privateKey", false)) + privateKey, err := crypto.NewKeyFromArmored(readTestFile("mime_privateKey", false)) if err != nil { t.Fatal("Cannot unarmor private key:", err) } @@ -50,37 +52,15 @@ func TestDecrypt(t *testing.T) { t.Fatal("Cannot unlock private key:", err) } - privateKeyRing, err := NewKeyRing(privateKey) + privateKeyRing, err := crypto.NewKeyRing(privateKey) if err != nil { t.Fatal("Cannot create private keyring:", err) } - message, err := NewPGPMessageFromArmored(readTestFile("mime_pgpMessage", false)) - if err != nil { - t.Fatal("Cannot decode armored message:", err) - } - - privateKeyRing.DecryptMIMEMessage( - message, - nil, - &callbacks, - GetUnixTime()) -} - -func TestParse(t *testing.T) { - body, atts, attHeaders, err := parseMIME(readTestFile("mime_testMessage", false), nil) - - if err != nil { - t.Fatal("Expected no error while parsing message, got:", err) - } - - _ = atts - _ = attHeaders - - bodyData, _ := body.GetBody() - assert.Exactly(t, readTestFile("mime_decodedBody", true), bodyData) - assert.Exactly(t, readTestFile("mime_decodedBodyHeaders", false), body.GetHeaders()) - assert.Exactly(t, 2, len(atts)) + message := readTestFileBytes("mime_pgpMessage") + pgp := crypto.PGP() + decHandle, _ := pgp.Decryption().DecryptionKeys(privateKeyRing).New() + Decrypt(message, crypto.Armor, decHandle, nil, &callbacks) } type testMIMECallbacks struct { @@ -120,32 +100,28 @@ func (tc *testMIMECallbacks) OnError(err error) { tc.onError = append(tc.onError, err) } -func loadPrivateKeyRing(file string, passphrase string) (*KeyRing, error) { - armored, err := ioutil.ReadFile(filepath.Clean(file)) +func loadPrivateKeyRing(file string, passphrase string) (*crypto.KeyRing, error) { + armored, err := os.ReadFile(filepath.Clean(file)) if err != nil { return nil, err } - key, err := NewKeyFromArmored(string(armored)) + unlockedKey, err := crypto.NewPrivateKeyFromArmored(string(armored), []byte(passphrase)) if err != nil { return nil, err } - unlockedKey, err := key.Unlock([]byte(passphrase)) - if err != nil { - return nil, err - } - keyRing, err := NewKeyRing(unlockedKey) + keyRing, err := crypto.NewKeyRing(unlockedKey) if err != nil { return nil, err } return keyRing, nil } -func loadPublicKeyRing(file string) (*KeyRing, error) { - armored, err := ioutil.ReadFile(filepath.Clean(file)) +func loadPublicKeyRing(file string) (*crypto.KeyRing, error) { + armored, err := os.ReadFile(filepath.Clean(file)) if err != nil { return nil, err } - key, err := NewKeyFromArmored(string(armored)) + key, err := crypto.NewKeyFromArmored(string(armored)) if err != nil { return nil, err } @@ -154,28 +130,24 @@ func loadPublicKeyRing(file string) (*KeyRing, error) { if err != nil { return nil, err } - key, err = NewKey(publicKey) + key, err = crypto.NewKey(publicKey) if err != nil { return nil, err } } - keyRing, err := NewKeyRing(key) + keyRing, err := crypto.NewKeyRing(key) if err != nil { return nil, err } return keyRing, nil } -func loadMessage(file string) (*PGPMessage, error) { - armored, err := ioutil.ReadFile(filepath.Clean(file)) - if err != nil { - return nil, err - } - message, err := NewPGPMessageFromArmored(string(armored)) +func loadMessage(file string) ([]byte, error) { + armored, err := os.ReadFile(filepath.Clean(file)) if err != nil { return nil, err } - return message, nil + return armored, nil } func runScenario(t *testing.T, messageFile string) *testMIMECallbacks { @@ -192,7 +164,17 @@ func runScenario(t *testing.T, messageFile string) *testMIMECallbacks { t.Errorf("Failed to load message %v", err) } callbacks := &testMIMECallbacks{} - decryptionKeyRing.DecryptMIMEMessage(message, verificationKeyRing, callbacks, 0) + pgp := crypto.PGP() + decHandle, _ := pgp.Decryption(). + DecryptionKeys(decryptionKeyRing). + VerificationKeys(verificationKeyRing). + VerifyTime(1557754627). + New() + verifyHandle, _ := pgp.Verify(). + VerificationKeys(verificationKeyRing). + VerifyTime(1557754627). + New() + Decrypt(message, crypto.Armor, decHandle, verifyHandle, callbacks) return callbacks } @@ -264,7 +246,7 @@ func TestMessageVerificationNotSignedOk(t *testing.T) { } func checkIsSigErr(t *testing.T, err error) int { - sigErr := &SignatureVerificationError{} + sigErr := &crypto.SignatureVerificationError{} if errors.As(err, &sigErr) { return sigErr.Status } @@ -272,7 +254,7 @@ func checkIsSigErr(t *testing.T, err error) int { return -1 } -func compareErrors(expected []SignatureVerificationError, actual []error, t *testing.T) { +func compareErrors(expected []crypto.SignatureVerificationError, actual []error, t *testing.T) { if len(actual) != len(expected) { t.Errorf("Expected %v, got %v", expected, actual) } else { @@ -287,7 +269,7 @@ func compareErrors(expected []SignatureVerificationError, actual []error, t *tes func TestMessageVerificationNotSignedNotSigned(t *testing.T) { callbackResults := runScenario(t, "testdata/mime/scenario_11.asc") - var expectedErrors = []SignatureVerificationError{newSignatureNotSigned(), newSignatureNotSigned()} + var expectedErrors = []crypto.SignatureVerificationError{newSignatureNotSigned(), newSignatureNotSigned()} compareErrors(expectedErrors, callbackResults.onError, t) expectedStatus := []int{1} compareStatus(expectedStatus, callbackResults.onVerified, t) @@ -295,7 +277,7 @@ func TestMessageVerificationNotSignedNotSigned(t *testing.T) { func TestMessageVerificationNotSignedNoVerifier(t *testing.T) { callbackResults := runScenario(t, "testdata/mime/scenario_12.asc") - var expectedErrors = []SignatureVerificationError{newSignatureNotSigned(), newSignatureNoVerifier()} + var expectedErrors = []crypto.SignatureVerificationError{newSignatureNotSigned(), newSignatureNoVerifier()} compareErrors(expectedErrors, callbackResults.onError, t) expectedStatus := []int{2} compareStatus(expectedStatus, callbackResults.onVerified, t) @@ -303,7 +285,7 @@ func TestMessageVerificationNotSignedNoVerifier(t *testing.T) { func TestMessageVerificationNotSignedFailed(t *testing.T) { callbackResults := runScenario(t, "testdata/mime/scenario_13.asc") - var expectedErrors = []SignatureVerificationError{newSignatureNotSigned(), newSignatureFailed(nil)} + var expectedErrors = []crypto.SignatureVerificationError{newSignatureNotSigned(), newSignatureFailed(nil)} compareErrors(expectedErrors, callbackResults.onError, t) expectedStatus := []int{3} compareStatus(expectedStatus, callbackResults.onVerified, t) @@ -311,7 +293,7 @@ func TestMessageVerificationNotSignedFailed(t *testing.T) { func TestMessageVerificationNoVerifierOk(t *testing.T) { callbackResults := runScenario(t, "testdata/mime/scenario_20.asc") - var expectedErrors = []SignatureVerificationError{} + var expectedErrors = []crypto.SignatureVerificationError{} compareErrors(expectedErrors, callbackResults.onError, t) expectedStatus := []int{0} compareStatus(expectedStatus, callbackResults.onVerified, t) @@ -319,7 +301,7 @@ func TestMessageVerificationNoVerifierOk(t *testing.T) { func TestMessageVerificationNoVerifierNotSigned(t *testing.T) { callbackResults := runScenario(t, "testdata/mime/scenario_21.asc") - var expectedErrors = []SignatureVerificationError{newSignatureNoVerifier(), newSignatureNotSigned()} + var expectedErrors = []crypto.SignatureVerificationError{newSignatureNoVerifier(), newSignatureNotSigned()} compareErrors(expectedErrors, callbackResults.onError, t) expectedStatus := []int{2} compareStatus(expectedStatus, callbackResults.onVerified, t) @@ -327,7 +309,7 @@ func TestMessageVerificationNoVerifierNotSigned(t *testing.T) { func TestMessageVerificationNoVerifierNoVerifier(t *testing.T) { callbackResults := runScenario(t, "testdata/mime/scenario_22.asc") - var expectedErrors = []SignatureVerificationError{newSignatureNoVerifier(), newSignatureNoVerifier()} + var expectedErrors = []crypto.SignatureVerificationError{newSignatureNoVerifier(), newSignatureNoVerifier()} compareErrors(expectedErrors, callbackResults.onError, t) expectedStatus := []int{2} compareStatus(expectedStatus, callbackResults.onVerified, t) @@ -335,8 +317,24 @@ func TestMessageVerificationNoVerifierNoVerifier(t *testing.T) { func TestMessageVerificationNoVerifierFailed(t *testing.T) { callbackResults := runScenario(t, "testdata/mime/scenario_23.asc") - var expectedErrors = []SignatureVerificationError{newSignatureNoVerifier(), newSignatureFailed(nil)} + var expectedErrors = []crypto.SignatureVerificationError{newSignatureNoVerifier(), newSignatureFailed(nil)} compareErrors(expectedErrors, callbackResults.onError, t) expectedStatus := []int{3} compareStatus(expectedStatus, callbackResults.onVerified, t) } + +func readTestFile(name string, trimNewlines bool) string { + data := string(readTestFileBytes(name)) + if trimNewlines { + return strings.TrimRight(data, "\n") + } + return data +} + +func readTestFileBytes(name string) []byte { + data, err := os.ReadFile(filepath.Join("testdata/", name)) //nolint:gosec + if err != nil { + panic(err) + } + return data +} diff --git a/mime/signature_collector.go b/mime/signature_collector.go new file mode 100644 index 00000000..8044288e --- /dev/null +++ b/mime/signature_collector.go @@ -0,0 +1,145 @@ +package mime + +import ( + "bytes" + "io" + "mime" + "net/textproto" + + pgpErrors "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/gopenpgp/v3/constants" + "github.com/ProtonMail/gopenpgp/v3/crypto" + "github.com/ProtonMail/gopenpgp/v3/internal" + + gomime "github.com/ProtonMail/go-mime" + "github.com/pkg/errors" +) + +// signatureCollector structure. +type signatureCollector struct { + verifyHandle crypto.PGPVerify + target gomime.VisitAcceptor + signature string + verified error +} + +func newSignatureCollector( + targetAcceptor gomime.VisitAcceptor, handle crypto.PGPVerify, +) *signatureCollector { + return &signatureCollector{ + target: targetAcceptor, + verifyHandle: handle, + } +} + +// Accept collects the signature. +func (sc *signatureCollector) Accept( + part io.Reader, header textproto.MIMEHeader, + hasPlainSibling, isFirst, isLast bool, +) (err error) { + parentMediaType, params, _ := mime.ParseMediaType(header.Get("Content-Type")) + + if parentMediaType != "multipart/signed" { + sc.verified = newSignatureNotSigned() + return sc.target.Accept(part, header, hasPlainSibling, isFirst, isLast) + } + + newPart, rawBody := gomime.GetRawMimePart(part, "--"+params["boundary"]) + multiparts, multipartHeaders, err := gomime.GetMultipartParts(newPart, params) + if err != nil { + return err + } + + hasPlainChild := false + for _, header := range multipartHeaders { + mediaType, _, _ := mime.ParseMediaType(header.Get("Content-Type")) + hasPlainChild = (mediaType == "text/plain") + } + if len(multiparts) != 2 { + sc.verified = newSignatureNotSigned() + // Invalid multipart/signed format just pass along + if _, err = io.ReadAll(rawBody); err != nil { + return errors.Wrap(err, "mime: error in reading raw message body") + } + + for i, p := range multiparts { + if err = sc.target.Accept(p, multipartHeaders[i], hasPlainChild, true, true); err != nil { + return err + } + } + return nil + } + + // actual multipart/signed format + err = sc.target.Accept(multiparts[0], multipartHeaders[0], hasPlainChild, true, true) + if err != nil { + return errors.Wrap(err, "mime: error in parsing body") + } + + partData, err := io.ReadAll(multiparts[1]) + if err != nil { + return errors.Wrap(err, "mime: error in ready part data") + } + + decodedPart := gomime.DecodeContentEncoding( + bytes.NewReader(partData), + multipartHeaders[1].Get("Content-Transfer-Encoding")) + + buffer, err := io.ReadAll(decodedPart) + if err != nil { + return errors.Wrap(err, "mime: error in reading decoded data") + } + mediaType, _, _ := mime.ParseMediaType(header.Get("Content-Type")) + buffer, err = gomime.DecodeCharset(buffer, mediaType, params) + if err != nil { + return errors.Wrap(err, "mime: error in decoding charset") + } + sc.signature = string(buffer) + str, _ := io.ReadAll(rawBody) + canonicalizedBody := internal.CanonicalizeBytes(internal.TrimEachLineBytes(str)) + if sc.verifyHandle != nil { + verifyResult, err := sc.verifyHandle.VerifyDetached(canonicalizedBody, buffer, crypto.Armor) + if errors.Is(err, pgpErrors.ErrUnknownIssuer) { + return newSignatureNoVerifier() + } + if err != nil { + return errors.Wrap(err, "mime: signature verification failed") + } + sc.verified = verifyResult.SignatureError() + } else { + sc.verified = newSignatureNoVerifier() + } + + return nil +} + +// GetSignature collected by Accept. +func (sc signatureCollector) GetSignature() string { + return sc.signature +} + +// newSignatureNotSigned creates a new SignatureVerificationError, type +// SignatureNotSigned. +func newSignatureNotSigned() crypto.SignatureVerificationError { + return crypto.SignatureVerificationError{ + Status: constants.SIGNATURE_NOT_SIGNED, + Message: "Missing signature", + } +} + +// newSignatureNoVerifier creates a new SignatureVerificationError, type +// SignatureNoVerifier. +func newSignatureNoVerifier() crypto.SignatureVerificationError { + return crypto.SignatureVerificationError{ + Status: constants.SIGNATURE_NO_VERIFIER, + Message: "No matching signature", + } +} + +func newSignatureFailed(cause error) crypto.SignatureVerificationError { + return crypto.SignatureVerificationError{ + Status: constants.SIGNATURE_FAILED, + Message: "Invalid signature", + Cause: cause, + } +} diff --git a/crypto/testdata/mime/decryption-key.asc b/mime/testdata/mime/decryption-key.asc similarity index 100% rename from crypto/testdata/mime/decryption-key.asc rename to mime/testdata/mime/decryption-key.asc diff --git a/crypto/testdata/mime/scenario_00.asc b/mime/testdata/mime/scenario_00.asc similarity index 100% rename from crypto/testdata/mime/scenario_00.asc rename to mime/testdata/mime/scenario_00.asc diff --git a/crypto/testdata/mime/scenario_01.asc b/mime/testdata/mime/scenario_01.asc similarity index 100% rename from crypto/testdata/mime/scenario_01.asc rename to mime/testdata/mime/scenario_01.asc diff --git a/crypto/testdata/mime/scenario_02.asc b/mime/testdata/mime/scenario_02.asc similarity index 100% rename from crypto/testdata/mime/scenario_02.asc rename to mime/testdata/mime/scenario_02.asc diff --git a/crypto/testdata/mime/scenario_03.asc b/mime/testdata/mime/scenario_03.asc similarity index 100% rename from crypto/testdata/mime/scenario_03.asc rename to mime/testdata/mime/scenario_03.asc diff --git a/crypto/testdata/mime/scenario_10.asc b/mime/testdata/mime/scenario_10.asc similarity index 100% rename from crypto/testdata/mime/scenario_10.asc rename to mime/testdata/mime/scenario_10.asc diff --git a/crypto/testdata/mime/scenario_11.asc b/mime/testdata/mime/scenario_11.asc similarity index 100% rename from crypto/testdata/mime/scenario_11.asc rename to mime/testdata/mime/scenario_11.asc diff --git a/crypto/testdata/mime/scenario_12.asc b/mime/testdata/mime/scenario_12.asc similarity index 100% rename from crypto/testdata/mime/scenario_12.asc rename to mime/testdata/mime/scenario_12.asc diff --git a/crypto/testdata/mime/scenario_13.asc b/mime/testdata/mime/scenario_13.asc similarity index 100% rename from crypto/testdata/mime/scenario_13.asc rename to mime/testdata/mime/scenario_13.asc diff --git a/crypto/testdata/mime/scenario_20.asc b/mime/testdata/mime/scenario_20.asc similarity index 100% rename from crypto/testdata/mime/scenario_20.asc rename to mime/testdata/mime/scenario_20.asc diff --git a/crypto/testdata/mime/scenario_21.asc b/mime/testdata/mime/scenario_21.asc similarity index 100% rename from crypto/testdata/mime/scenario_21.asc rename to mime/testdata/mime/scenario_21.asc diff --git a/crypto/testdata/mime/scenario_22.asc b/mime/testdata/mime/scenario_22.asc similarity index 100% rename from crypto/testdata/mime/scenario_22.asc rename to mime/testdata/mime/scenario_22.asc diff --git a/crypto/testdata/mime/scenario_23.asc b/mime/testdata/mime/scenario_23.asc similarity index 100% rename from crypto/testdata/mime/scenario_23.asc rename to mime/testdata/mime/scenario_23.asc diff --git a/crypto/testdata/mime/verification-key.asc b/mime/testdata/mime/verification-key.asc similarity index 100% rename from crypto/testdata/mime/verification-key.asc rename to mime/testdata/mime/verification-key.asc diff --git a/crypto/testdata/mime_decodedBody b/mime/testdata/mime_decodedBody similarity index 100% rename from crypto/testdata/mime_decodedBody rename to mime/testdata/mime_decodedBody diff --git a/crypto/testdata/mime_decodedBodyHeaders b/mime/testdata/mime_decodedBodyHeaders similarity index 100% rename from crypto/testdata/mime_decodedBodyHeaders rename to mime/testdata/mime_decodedBodyHeaders diff --git a/crypto/testdata/mime_decryptedBody b/mime/testdata/mime_decryptedBody similarity index 100% rename from crypto/testdata/mime_decryptedBody rename to mime/testdata/mime_decryptedBody diff --git a/crypto/testdata/mime_pgpMessage b/mime/testdata/mime_pgpMessage similarity index 100% rename from crypto/testdata/mime_pgpMessage rename to mime/testdata/mime_pgpMessage diff --git a/crypto/testdata/mime_privateKey b/mime/testdata/mime_privateKey similarity index 100% rename from crypto/testdata/mime_privateKey rename to mime/testdata/mime_privateKey diff --git a/crypto/testdata/mime_publicKey b/mime/testdata/mime_publicKey similarity index 100% rename from crypto/testdata/mime_publicKey rename to mime/testdata/mime_publicKey diff --git a/crypto/testdata/mime_testMessage b/mime/testdata/mime_testMessage similarity index 100% rename from crypto/testdata/mime_testMessage rename to mime/testdata/mime_testMessage diff --git a/mobile/mobile.go b/mobile/mobile.go new file mode 100644 index 00000000..d745a1b5 --- /dev/null +++ b/mobile/mobile.go @@ -0,0 +1,11 @@ +// Package mobile provides tools for mobile compatibility. +package mobile + +import "runtime/debug" + +// FreeOSMemory can be used to explicitly +// call the garbage collector and +// return the unused memory to the OS. +func FreeOSMemory() { + debug.FreeOSMemory() +} diff --git a/helper/mobile_stream.go b/mobile/mobile_stream.go similarity index 73% rename from helper/mobile_stream.go rename to mobile/mobile_stream.go index 9fa927c4..14fdfc8c 100644 --- a/helper/mobile_stream.go +++ b/mobile/mobile_stream.go @@ -1,11 +1,12 @@ -package helper +package mobile import ( + "bytes" "crypto/sha256" "hash" "io" - "github.com/ProtonMail/gopenpgp/v2/crypto" + "github.com/ProtonMail/gopenpgp/v3/crypto" "github.com/pkg/errors" ) @@ -181,23 +182,64 @@ func (r *Go2IOSReader) Read(max int) (result *MobileReadResult, err error) { return result, nil } -// VerifySignatureExplicit calls the reader's VerifySignature() -// and tries to cast the returned error to a SignatureVerificationError. -func VerifySignatureExplicit( - reader *crypto.PlainMessageReader, -) (signatureVerificationError *crypto.SignatureVerificationError, err error) { - if reader == nil { - return nil, errors.New("gopenppg: the reader can't be nil") +// KeyPacketSplitWriter implements the crypto.PGPSplitWriter interface +// for splitting encryptions output into different packets. +// Internally buffers the key packets and potential detached encrypted signatures. +type KeyPacketSplitWriter struct { + dataWriter crypto.Writer + keyPacket *bytes.Buffer + encDetachedSignature *bytes.Buffer +} + +func NewKeyPacketSplitWriter(dataWriter crypto.Writer) *KeyPacketSplitWriter { + return &KeyPacketSplitWriter{ + dataWriter: dataWriter, + keyPacket: bytes.NewBuffer(nil), } - err = reader.VerifySignature() - if err != nil { - castedErr := &crypto.SignatureVerificationError{} - isType := errors.As(err, castedErr) - if !isType { - return - } - signatureVerificationError = castedErr - err = nil +} + +// KeyPackets returns the internally buffered key packets. +func (sw *KeyPacketSplitWriter) KeyPackets() []byte { + return sw.keyPacket.Bytes() +} + +// EncryptedDetachedSignature returns the internally buffered encrypted detached signature. +func (sw *KeyPacketSplitWriter) EncryptedDetachedSignature() *crypto.PGPMessage { + return crypto.NewPGPSplitMessage(sw.keyPacket.Bytes(), sw.encDetachedSignature.Bytes()) +} + +// Implement the crypto.PGPSplitWriter interface. + +func (sw *KeyPacketSplitWriter) Write(b []byte) (n int, err error) { + return sw.dataWriter.Write(b) +} + +func (sw *KeyPacketSplitWriter) Keys() crypto.Writer { + return sw.keyPacket +} + +func (sw *KeyPacketSplitWriter) Signature() crypto.Writer { + return sw.encDetachedSignature +} + +// DetachedSignaturePGPSplitReader implements the crypto.PGPSplitReader interface. +type DetachedSignaturePGPSplitReader struct { + dataReader crypto.Reader + encDetachedSignature crypto.Reader +} + +func NewDetachedSignaturePGPSplitReader(keyPacket []byte, dataReader crypto.Reader, encSignature *crypto.PGPMessage) *DetachedSignaturePGPSplitReader { + internalDataReader := io.MultiReader(bytes.NewReader(keyPacket), dataReader) + return &DetachedSignaturePGPSplitReader{ + dataReader: internalDataReader, + encDetachedSignature: encSignature.NewReader(), } - return +} + +func (ds *DetachedSignaturePGPSplitReader) Read(b []byte) (n int, err error) { + return ds.dataReader.Read(b) +} + +func (ds *DetachedSignaturePGPSplitReader) Signature() crypto.Reader { + return ds.encDetachedSignature } diff --git a/helper/mobile_stream_test.go b/mobile/mobile_stream_test.go similarity index 73% rename from helper/mobile_stream_test.go rename to mobile/mobile_stream_test.go index a3ab85e3..9db680bd 100644 --- a/helper/mobile_stream_test.go +++ b/mobile/mobile_stream_test.go @@ -1,15 +1,15 @@ -package helper +package mobile import ( "bytes" "crypto/sha256" "errors" "io" - "io/ioutil" "testing" - "github.com/ProtonMail/gopenpgp/v2/constants" - "github.com/ProtonMail/gopenpgp/v2/crypto" + "github.com/ProtonMail/gopenpgp/v3/constants" + "github.com/ProtonMail/gopenpgp/v3/crypto" + "github.com/ProtonMail/gopenpgp/v3/profile" ) func cloneTestData() (a, b []byte) { @@ -185,52 +185,57 @@ func TestMobile2GoReader(t *testing.T) { } } -func setUpTestKeyRing() (*crypto.KeyRing, *crypto.KeyRing, error) { - testKey, err := crypto.GenerateKey("test", "test@protonmail.com", "x25519", 256) +func setUpTestKeyRing() (*crypto.PGPHandle, *crypto.KeyRing, *crypto.KeyRing, error) { + pgpHandle := crypto.PGPWithProfile(profile.Default()) + testKey, err := pgpHandle.KeyGeneration(). + AddUserId("test", "test@protonmail.com"). + New(). + GenerateKey() if err != nil { - return nil, nil, err + return nil, nil, nil, err } testPublicKey, err := testKey.ToPublic() if err != nil { - return nil, nil, err + return nil, nil, nil, err } testPrivateKeyRing, err := crypto.NewKeyRing(testKey) if err != nil { - return nil, nil, err + return nil, nil, nil, err } testPublicKeyRing, err := crypto.NewKeyRing(testPublicKey) if err != nil { - return nil, nil, err + return nil, nil, nil, err } - return testPublicKeyRing, testPrivateKeyRing, nil + return pgpHandle, testPublicKeyRing, testPrivateKeyRing, nil } func TestExplicitVerifyAllGoesWell(t *testing.T) { data := []byte("hello") - pubKR, privKR, err := setUpTestKeyRing() + pgpHandle, pubKR, privKR, err := setUpTestKeyRing() if err != nil { t.Fatalf("Got an error while loading test key: %v", err) } defer privKR.ClearPrivateParams() - ciphertext, err := pubKR.Encrypt(crypto.NewPlainMessage(data), privKR) + encHandle, _ := pgpHandle.Encryption().Recipients(pubKR).SigningKeys(privKR).New() + ciphertext, err := encHandle.Encrypt(data) if err != nil { t.Fatalf("Got an error while encrypting test data: %v", err) } - reader, err := privKR.DecryptStream( - bytes.NewReader(ciphertext.Data), - pubKR, - crypto.GetUnixTime(), - ) + decHandle, _ := pgpHandle.Decryption().DecryptionKeys(privKR).VerificationKeys(pubKR).New() + reader, err := decHandle.DecryptingReader(bytes.NewReader(ciphertext.Bytes()), crypto.Bytes) if err != nil { t.Fatalf("Got an error while decrypting stream data: %v", err) } - _, err = ioutil.ReadAll(reader) + _, err = io.ReadAll(reader) if err != nil { t.Fatalf("Got an error while reading decrypted data: %v", err) } - sigErr, err := VerifySignatureExplicit(reader) - if sigErr != nil { - t.Fatalf("Got a signature error while verifying embedded sig: %v", sigErr) + sigErr, err := reader.VerifySignature() + if err != nil { + t.Fatalf("Got an error while decrypting: %v", err) + } + if err = sigErr.SignatureError(); err != nil { + t.Fatalf("Got a signature error while verifying embedded sig: %v", err) } if err != nil { t.Fatalf("Got an error while verifying embedded sig: %v", err) @@ -239,20 +244,18 @@ func TestExplicitVerifyAllGoesWell(t *testing.T) { func TestExplicitVerifyTooEarly(t *testing.T) { data := []byte("hello") - pubKR, privKR, err := setUpTestKeyRing() + pgp, pubKR, privKR, err := setUpTestKeyRing() if err != nil { t.Fatalf("Got an error while loading test key: %v", err) } defer privKR.ClearPrivateParams() - ciphertext, err := pubKR.Encrypt(crypto.NewPlainMessage(data), privKR) + encHandle, _ := pgp.Encryption().Recipients(pubKR).SigningKeys(privKR).New() + ciphertext, err := encHandle.Encrypt(data) if err != nil { t.Fatalf("Got an error while encrypting test data: %v", err) } - reader, err := privKR.DecryptStream( - bytes.NewReader(ciphertext.Data), - pubKR, - crypto.GetUnixTime(), - ) + decHandle, _ := pgp.Decryption().DecryptionKeys(privKR).VerificationKeys(pubKR).New() + reader, err := decHandle.DecryptingReader(bytes.NewReader(ciphertext.Bytes()), crypto.Bytes) if err != nil { t.Fatalf("Got an error while decrypting stream data: %v", err) } @@ -261,86 +264,82 @@ func TestExplicitVerifyTooEarly(t *testing.T) { if err != nil { t.Fatalf("Got an error while reading decrypted data: %v", err) } - sigErr, err := VerifySignatureExplicit(reader) - if sigErr != nil { - t.Fatalf("Got a signature error while verifying embedded sig: %v", sigErr) - } + sigErr, err := reader.VerifySignature() if err == nil { t.Fatalf("Got no error while verifying a reader before reading it entirely") } + if err = sigErr.SignatureError(); err != nil { + t.Fatalf("Got a signature error while verifying embedded sig: %v", sigErr.SignatureError()) + } } func TestExplicitVerifyNoSig(t *testing.T) { data := []byte("hello") - pubKR, privKR, err := setUpTestKeyRing() + pgp, pubKR, privKR, err := setUpTestKeyRing() if err != nil { t.Fatalf("Got an error while loading test key: %v", err) } defer privKR.ClearPrivateParams() - ciphertext, err := pubKR.Encrypt(crypto.NewPlainMessage(data), nil) + encHandle, _ := pgp.Encryption().Recipients(pubKR).New() + ciphertext, err := encHandle.Encrypt(data) if err != nil { t.Fatalf("Got an error while encrypting test data: %v", err) } - reader, err := privKR.DecryptStream( - bytes.NewReader(ciphertext.Data), - pubKR, - crypto.GetUnixTime(), - ) + decHandle, _ := pgp.Decryption().DecryptionKeys(privKR).VerificationKeys(pubKR).New() + reader, err := decHandle.DecryptingReader(bytes.NewReader(ciphertext.Bytes()), crypto.Bytes) if err != nil { t.Fatalf("Got an error while decrypting stream data: %v", err) } - _, err = ioutil.ReadAll(reader) + _, err = io.ReadAll(reader) if err != nil { t.Fatalf("Got an error while reading decrypted data: %v", err) } - sigErr, err := VerifySignatureExplicit(reader) - if sigErr == nil { + sigErr, err := reader.VerifySignature() + if err != nil { + t.Fatalf("Got an error while verifying embedded sig: %v", err) + } + if err = sigErr.SignatureError(); err == nil { t.Fatal("Got no signature error while verifying unsigned data") } - if sigErr.Status != constants.SIGNATURE_NOT_SIGNED { + if sigErr.SignatureErrorExplicit().Status != constants.SIGNATURE_NOT_SIGNED { t.Fatal("Signature error status was not SIGNATURE_NOT_SIGNED") } - if err != nil { - t.Fatalf("Got an error while verifying embedded sig: %v", err) - } } func TestExplicitVerifyWrongVerifier(t *testing.T) { data := []byte("hello") - pubKR, privKR, err := setUpTestKeyRing() + pgp, pubKR, privKR, err := setUpTestKeyRing() if err != nil { t.Fatalf("Got an error while loading test key: %v", err) } defer privKR.ClearPrivateParams() - _, privKR2, err := setUpTestKeyRing() + _, _, privKR2, err := setUpTestKeyRing() if err != nil { t.Fatalf("Got an error while loading test key: %v", err) } defer privKR2.ClearPrivateParams() - ciphertext, err := pubKR.Encrypt(crypto.NewPlainMessage(data), privKR2) + encHandle, _ := pgp.Encryption().Recipients(pubKR).SigningKeys(privKR2).New() + ciphertext, err := encHandle.Encrypt(data) if err != nil { t.Fatalf("Got an error while encrypting test data: %v", err) } - reader, err := privKR.DecryptStream( - bytes.NewReader(ciphertext.Data), - pubKR, - crypto.GetUnixTime(), - ) + decHandle, _ := pgp.Decryption().DecryptionKeys(privKR).VerificationKeys(pubKR).New() + reader, err := decHandle.DecryptingReader(bytes.NewReader(ciphertext.Bytes()), crypto.Bytes) if err != nil { t.Fatalf("Got an error while decrypting stream data: %v", err) } - _, err = ioutil.ReadAll(reader) + _, err = io.ReadAll(reader) if err != nil { t.Fatalf("Got an error while reading decrypted data: %v", err) } - sigErr, err := VerifySignatureExplicit(reader) - if sigErr == nil { + sigErr, err := reader.VerifySignature() + if err != nil { + t.Fatalf("Got an error while verifying embedded sig: %v", err) + } + if err = sigErr.SignatureError(); err == nil { t.Fatal("Got no signature error while verifying with wrong key") } - if sigErr.Status != constants.SIGNATURE_NO_VERIFIER { + if sigErr.SignatureErrorExplicit().Status != constants.SIGNATURE_NO_VERIFIER { t.Fatal("Signature error status was not SIGNATURE_NO_VERIFIER") } - if err != nil { - t.Fatalf("Got an error while verifying embedded sig: %v", err) - } } diff --git a/models/models.go b/models/models.go deleted file mode 100644 index 7aa6018c..00000000 --- a/models/models.go +++ /dev/null @@ -1,8 +0,0 @@ -// Package models provides structs containing message data. -package models - -// EncryptedSigned contains an encrypted message and signature. -type EncryptedSigned struct { - Encrypted string - Signature string -} diff --git a/profile/preset.go b/profile/preset.go new file mode 100644 index 00000000..ac95f0e6 --- /dev/null +++ b/profile/preset.go @@ -0,0 +1,85 @@ +package profile + +import ( + "crypto" + + "github.com/ProtonMail/go-crypto/openpgp/packet" + "github.com/ProtonMail/go-crypto/openpgp/s2k" + "github.com/ProtonMail/gopenpgp/v3/constants" +) + +// Default returns a custom profile that support features +// that are widely implemented. +func Default() *Custom { + setKeyAlgorithm := func(cfg *packet.Config, securityLevel int8) { + cfg.Algorithm = packet.PubKeyAlgoEdDSA + switch securityLevel { + case constants.HighSecurity: + cfg.Curve = packet.Curve25519 + default: + cfg.Curve = packet.Curve25519 + } + } + return &Custom{ + Name: "default", + SetKeyAlgorithm: setKeyAlgorithm, + Hash: crypto.SHA256, + CipherEncryption: packet.CipherAES256, + CompressionAlgorithm: packet.CompressionZLIB, + CompressionConfiguration: &packet.CompressionConfig{ + Level: 6, + }, + } +} + +// RFC4880 returns a custom profile for this library +// that conforms with the algorithms in RFC4880. +func RFC4880() *Custom { + setKeyAlgorithm := func(cfg *packet.Config, securityLevel int8) { + cfg.Algorithm = packet.PubKeyAlgoRSA + switch securityLevel { + case constants.HighSecurity: + cfg.RSABits = 4096 + default: + cfg.RSABits = 3072 + } + } + return &Custom{ + Name: "rfc4880", + SetKeyAlgorithm: setKeyAlgorithm, + Hash: crypto.SHA256, + CipherEncryption: packet.CipherAES256, + CompressionAlgorithm: packet.CompressionZLIB, + } +} + +// RFC9580 returns a custom profile for this library +// that conforms with the algorithms in RFC9580 (crypto refresh). +func RFC9580() *Custom { + setKeyAlgorithm := func(cfg *packet.Config, securityLevel int8) { + switch securityLevel { + case constants.HighSecurity: + cfg.Algorithm = packet.PubKeyAlgoEd448 + default: + cfg.Algorithm = packet.PubKeyAlgoEd25519 + } + } + return &Custom{ + Name: "rfc9580", + SetKeyAlgorithm: setKeyAlgorithm, + Hash: crypto.SHA512, + CipherEncryption: packet.CipherAES256, + CompressionAlgorithm: packet.CompressionZLIB, + AeadKeyEncryption: &packet.AEADConfig{}, + AeadEncryption: &packet.AEADConfig{}, + S2kKeyEncryption: &s2k.Config{ + S2KMode: s2k.Argon2S2K, + Argon2Config: &s2k.Argon2Config{}, + }, + S2kEncryption: &s2k.Config{ + S2KMode: s2k.Argon2S2K, + Argon2Config: &s2k.Argon2Config{}, + }, + V6: true, + } +} diff --git a/profile/profile.go b/profile/profile.go new file mode 100644 index 00000000..d86930b3 --- /dev/null +++ b/profile/profile.go @@ -0,0 +1,125 @@ +// Package profile provides different profiles to run GopenPGP. +package profile + +import ( + "crypto" + + "github.com/ProtonMail/go-crypto/openpgp/packet" + "github.com/ProtonMail/go-crypto/openpgp/s2k" +) + +const weakMinRSABits = 1023 + +// Custom type represents a profile for setting algorithm +// parameters for generating keys, encrypting data, and +// signing data. +// Use one of the pre-defined profiles if possible. +// i.e., profile.Default(), profile.RFC4880(). +type Custom struct { + // Name defines the name of the custom profile. + Name string + // SetKeyAlgorithm is a function that sets public key encryption + // algorithm in the config bases on the int8 security level. + SetKeyAlgorithm func(*packet.Config, int8) + // AeadKeyEncryption defines the aead encryption algorithm for key encryption. + AeadKeyEncryption *packet.AEADConfig + // S2kKeyEncryption defines the s2k algorithm for key encryption. + S2kKeyEncryption *s2k.Config + // AeadEncryption defines the aead encryption algorithm for pgp encryption. + AeadEncryption *packet.AEADConfig + // S2kEncryption defines the s2k algorithm for pgp encryption. + S2kEncryption *s2k.Config + // CompressionConfiguration defines the compression configuration to be used if any. + CompressionConfiguration *packet.CompressionConfig + // Hash defines hash algorithm to be used. + Hash crypto.Hash + // SignHash defines if a different hash algorithm should be used for signing. + // If nil, the a above field Hash is used. + SignHash *crypto.Hash + // CipherKeyEncryption defines the cipher to be used for key encryption. + CipherKeyEncryption packet.CipherFunction + // CipherEncryption defines the cipher to be used for pgp message encryption. + CipherEncryption packet.CipherFunction + // CompressionAlgorithm defines the compression algorithm to be used if any. + CompressionAlgorithm packet.CompressionAlgo + // V6 is a flag to indicate if v6 from the crypto-refresh should be used. + V6 bool + // AllowAllPublicKeyAlgorithms is a flag to disable all checks for deprecated public key algorithms. + AllowAllPublicKeyAlgorithms bool + // DisableIntendedRecipients is a flag to disable the intended recipients pgp feature from the crypto-refresh. + DisableIntendedRecipients bool + // AllowWeakRSA is a flag to disable checks for weak rsa keys. + AllowWeakRSA bool +} + +// Custom implements the profile interfaces: +// KeyGenerationProfile, KeyEncryptionProfile, EncryptionProfile, and SignProfile + +func (p *Custom) KeyGenerationConfig(securityLevel int8) *packet.Config { + cfg := &packet.Config{ + DefaultHash: p.Hash, + DefaultCipher: p.CipherEncryption, + AEADConfig: p.AeadEncryption, + DefaultCompressionAlgo: p.CompressionAlgorithm, + CompressionConfig: p.CompressionConfiguration, + V6Keys: p.V6, + } + p.SetKeyAlgorithm(cfg, securityLevel) + return cfg +} + +func (p *Custom) EncryptionConfig() *packet.Config { + config := &packet.Config{ + DefaultHash: p.Hash, + DefaultCipher: p.CipherEncryption, + AEADConfig: p.AeadEncryption, + S2KConfig: p.S2kEncryption, + } + if p.DisableIntendedRecipients { + intendedRecipients := false + config.CheckIntendedRecipients = &intendedRecipients + } + if p.AllowAllPublicKeyAlgorithms { + config.RejectPublicKeyAlgorithms = map[packet.PublicKeyAlgorithm]bool{} + } + if p.AllowWeakRSA { + config.MinRSABits = weakMinRSABits + } + return config +} + +func (p *Custom) KeyEncryptionConfig() *packet.Config { + return &packet.Config{ + DefaultHash: p.Hash, + DefaultCipher: p.CipherKeyEncryption, + AEADConfig: p.AeadKeyEncryption, + S2KConfig: p.S2kKeyEncryption, + } +} + +func (p *Custom) SignConfig() *packet.Config { + config := &packet.Config{ + DefaultHash: p.Hash, + } + if p.SignHash != nil { + config.DefaultHash = *p.SignHash + } + if p.DisableIntendedRecipients { + intendedRecipients := false + config.CheckIntendedRecipients = &intendedRecipients + } + if p.AllowAllPublicKeyAlgorithms { + config.RejectPublicKeyAlgorithms = map[packet.PublicKeyAlgorithm]bool{} + } + if p.AllowWeakRSA { + config.MinRSABits = weakMinRSABits + } + return config +} + +func (p *Custom) CompressionConfig() *packet.Config { + return &packet.Config{ + CompressionConfig: p.CompressionConfiguration, + DefaultCompressionAlgo: p.CompressionAlgorithm, + } +} diff --git a/subtle/subtle.go b/subtle/subtle.go deleted file mode 100644 index 80cb2935..00000000 --- a/subtle/subtle.go +++ /dev/null @@ -1,35 +0,0 @@ -// Package subtle contains subtly insecure methods not recommended for casual -// use. -package subtle - -import ( - "crypto/aes" - "crypto/cipher" - - "golang.org/x/crypto/scrypt" -) - -// EncryptWithoutIntegrity encrypts data with AES-CTR. Note: this encryption -// mode is not secure when stored/sent on an untrusted medium. -func EncryptWithoutIntegrity(key, input, iv []byte) (output []byte, err error) { - var block cipher.Block - if block, err = aes.NewCipher(key); err != nil { - return - } - output = make([]byte, len(input)) - stream := cipher.NewCTR(block, iv) - stream.XORKeyStream(output, input) - return -} - -// DecryptWithoutIntegrity decrypts data encrypted with AES-CTR. -func DecryptWithoutIntegrity(key, input, iv []byte) ([]byte, error) { - // AES-CTR decryption is identical to encryption. - return EncryptWithoutIntegrity(key, input, iv) -} - -// DeriveKey derives a key from a password using scrypt. n should be set to the -// highest power of 2 you can derive within 100 milliseconds. -func DeriveKey(password string, salt []byte, n int) ([]byte, error) { - return scrypt.Key([]byte(password), salt, n, 8, 1, 32) -} diff --git a/subtle/subtle_test.go b/subtle/subtle_test.go deleted file mode 100644 index e253b24a..00000000 --- a/subtle/subtle_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package subtle - -import ( - "encoding/hex" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestSubtle_EncryptWithoutIntegrity(t *testing.T) { - key, _ := hex.DecodeString("9469cccfc8a8d005247f39fa3e5b35a97db456cecf18deac6d84364d0818d763") - plaintext := []byte("some plaintext") - iv, _ := hex.DecodeString("c828f258a76aad7bc828f258a76aad7b") - - ciphertext, _ := EncryptWithoutIntegrity(key, plaintext, iv) - assert.Exactly(t, "14697192f7e112fc88d83380693f", hex.EncodeToString(ciphertext)) -} - -func TestSubtle_DecryptWithoutIntegrity(t *testing.T) { - key, _ := hex.DecodeString("9469cccfc8a8d005247f39fa3e5b35a97db456cecf18deac6d84364d0818d763") - ciphertext, _ := hex.DecodeString("14697192f7e112fc88d83380693f") - iv, _ := hex.DecodeString("c828f258a76aad7bc828f258a76aad7b") - - plaintext, _ := DecryptWithoutIntegrity(key, ciphertext, iv) - assert.Exactly(t, "some plaintext", string(plaintext)) -} - -func TestSubtle_DeriveKey(t *testing.T) { - salt, _ := hex.DecodeString("c828f258a76aad7b") - dk, _ := DeriveKey("some password", salt, 32768) - assert.Exactly(t, "9469cccfc8a8d005247f39fa3e5b35a97db456cecf18deac6d84364d0818d763", hex.EncodeToString(dk)) -}