From 54f45d0471cf4b588a9cdc9ff013465d31196a01 Mon Sep 17 00:00:00 2001 From: wussler Date: Fri, 27 Dec 2019 19:35:43 +0100 Subject: [PATCH] Openpgp security update (V2) (#31) * Change keyring unlock functionalities * Add keyring#Lock, keyring#CheckIntegrity, tests * Update helpers, fix bugs * Update go.mod with ProtonMail/crypto commit * Change key management system * Clear keys from memory + tests * Create SessionKey with direct encryption for datapackets. Move symmetrickey to password. * Fix upstream dependencies * Update module to V2, documentation * Add linter * Add v2 folder to .gitignore * Minor changes to KeyID getters * Remove old changelog * Improve docs, remove compilation script --- .gitignore | 1 + .golangci.yml | 54 +++ .travis.yml | 39 +- CHANGELOG.md | 498 ++++++++++++++++++++++ Changelog.md | 17 - ProposalChanges.md | 474 -------------------- README.md | 251 ++++++----- armor/armor.go | 11 +- build.sh | 24 -- crypto/attachment.go | 10 +- crypto/attachment_test.go | 24 +- crypto/base_test.go | 57 ++- crypto/key.go | 455 ++++++++++++++------ crypto/key_clear.go | 128 ++++++ crypto/key_test.go | 330 ++++++++++---- crypto/keyring.go | 357 +++++----------- crypto/keyring_message.go | 19 +- crypto/{session.go => keyring_session.go} | 35 +- crypto/keyring_test.go | 228 +++++----- crypto/message.go | 51 ++- crypto/message_test.go | 109 ++--- crypto/mime.go | 2 +- crypto/mime_test.go | 17 +- crypto/password.go | 151 +++++++ crypto/session_test.go | 74 ---- crypto/sessionkey.go | 221 ++++++++++ crypto/sessionkey_test.go | 145 +++++++ crypto/signature.go | 14 +- crypto/signature_collector.go | 3 +- crypto/signature_test.go | 32 +- crypto/symmetrickey.go | 225 ---------- crypto/time.go | 10 +- go.mod | 11 +- go.sum | 20 +- helper/base_test.go | 12 +- helper/cleartext.go | 24 +- helper/cleartext_test.go | 4 +- helper/helper.go | 119 ++++-- helper/helper_test.go | 10 +- helper/ios.go | 13 +- helper/ios_test.go | 17 +- helper/key.go | 48 +++ internal/armor.go | 3 +- internal/common.go | 2 +- subtle/subtle.go | 6 +- subtle/subtle_test.go | 3 +- 46 files changed, 2588 insertions(+), 1770 deletions(-) create mode 100644 .golangci.yml create mode 100644 CHANGELOG.md delete mode 100644 Changelog.md delete mode 100644 ProposalChanges.md delete mode 100755 build.sh create mode 100644 crypto/key_clear.go rename crypto/{session.go => keyring_session.go} (56%) create mode 100644 crypto/password.go delete mode 100644 crypto/session_test.go create mode 100644 crypto/sessionkey.go create mode 100644 crypto/sessionkey_test.go delete mode 100644 crypto/symmetrickey.go create mode 100644 helper/key.go diff --git a/.gitignore b/.gitignore index 21657f8..33c4300 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ vendor *.html reports .idea +v2 \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..c6496d7 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,54 @@ +linters-settings: + godox: + keywords: # default keywords are TODO, BUG, and FIXME, but we override this by ignoring TODO + - BUG + - FIXME + funlen: + lines: 100 + statements: 80 + +issues: + exclude-use-default: false + exclude: + - Using the variable on range scope `tt` in function literal + +linters: + enable: + - deadcode # Finds unused code [fast: true, auto-fix: false] + - errcheck # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases [fast: true, auto-fix: false] + - gosimple # Linter for Go source code that specializes in simplifying a code [fast: true, auto-fix: false] + - govet # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string [fast: true, auto-fix: false] + - ineffassign # Detects when assignments to existing variables are not used [fast: true, auto-fix: false] + - staticcheck # Staticcheck is a go vet on steroids, applying a ton of static analysis checks [fast: true, auto-fix: false] + - structcheck # Finds unused struct fields [fast: true, auto-fix: false] + - typecheck # Like the front-end of a Go compiler, parses and type-checks Go code [fast: true, auto-fix: false] + - unused # Checks Go code for unused constants, variables, functions and types [fast: false, auto-fix: false] + - varcheck # Finds unused global variables and constants [fast: true, auto-fix: false] + - depguard # Go linter that checks if package imports are in a list of acceptable packages [fast: true, auto-fix: false] + - dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) [fast: true, auto-fix: false] +# - dupl # Tool for code clone detection [fast: true, auto-fix: false] + - funlen # Tool for detection of long functions [fast: true, auto-fix: false] +# - gochecknoglobals # Checks that no globals are present in Go code [fast: true, auto-fix: false] +# - gochecknoinits # Checks that no init functions are present in Go code [fast: true, auto-fix: false] + - goconst # Finds repeated strings that could be replaced by a constant [fast: true, auto-fix: false] + - gocritic # The most opinionated Go source code linter [fast: true, auto-fix: false] + - gocyclo # Computes and checks the cyclomatic complexity of functions [fast: true, auto-fix: false] + - godox # Tool for detection of FIXME, TODO and other comment keywords [fast: true, auto-fix: false] + - gofmt # Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification [fast: true, auto-fix: true] + - goimports # Goimports does everything that gofmt does. Additionally it checks unused imports [fast: true, auto-fix: true] +# - golint # Golint differs from gofmt. Gofmt reformats Go source code, whereas golint prints out style mistakes [fast: true, auto-fix: false] + - gosec # Inspects source code for security problems [fast: true, auto-fix: false] + - interfacer # Linter that suggests narrower interface types [fast: true, auto-fix: false] + - maligned # Tool to detect Go structs that would take less memory if their fields were sorted [fast: true, auto-fix: false] + - misspell # Finds commonly misspelled English words in comments [fast: true, auto-fix: true] + - nakedret # Finds naked returns in functions greater than a specified function length [fast: true, auto-fix: false] + - prealloc # Finds slice declarations that could potentially be preallocated [fast: true, auto-fix: false] + - scopelint # Scopelint checks for unpinned variables in go programs [fast: true, auto-fix: false] +# - stylecheck # Stylecheck is a replacement for golint [fast: true, auto-fix: false] + - unconvert # Remove unnecessary type conversions [fast: true, auto-fix: false] + - unparam # Reports unused function parameters [fast: true, auto-fix: false] + - whitespace # Tool for detection of leading and trailing whitespace [fast: true, auto-fix: true] + + disable: + - wsl # Whitespace Linter - Forces you to use empty lines! [fast: true, auto-fix: false] + - lll # Reports long lines [fast: true, auto-fix: false]\ diff --git a/.travis.yml b/.travis.yml index 510c082..3b524f9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,42 @@ +# use the latest ubuntu environment (18.04) available on travis +dist: bionic + language: go + +# Force-enable Go modules. Also force go to use the code in vendor/ +# These will both be unnecessary when Go 1.14 lands. +env: GO111MODULE=on + go: - 1.11.x - 1.12.x -env: - - GO111MODULE=on GOFLAGS=-mod=readonly + - 1.13.x + +# Only clone the most recent commit. +git: + depth: 1 + +# Skip the install step. Don't `go get` dependencies. Only build with the code +# in vendor/ install: true + +# Don't email me the results of the test runs. +notifications: + email: false + +# Anything in before_script that returns a nonzero exit code will flunk the +# build and immediately stop. It's sorta like having set -e enabled in bash. +# Make sure you've pinned the version of golangci-lint by running this command +# in your project directory: +# GO111MODULE=on go get github.com/golangci/golangci-lint@v1.21.0 +# You should see this line in your go.mod file: +# github.com/golangci/golangci-lint v1.21.0 +before_script: + - go install github.com/golangci/golangci-lint/cmd/golangci-lint + +# script always runs to completion (set +e). If we have linter issues AND a +# failing test, we want to see both. Configure golangci-lint with a +# .golangci.yml file at the top level of your repo. +script: + - golangci-lint run # run a bunch of code checkers/linters in parallel + - go test -v -race ./... # Run all the tests with the race detector enabled \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..fbaf266 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,498 @@ +# Changelog +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.0.0] - 2019-12-?? +Since the open-sourcing of the library in May the API has been updated, listening to internal and +external feedback, in order to have a flexible library, that can be used in a simple settings, +with batteries included, or by more advanced users that might want to interact directly with +the inner structure of the PGP messages and keys. + +It allows direct interaction with keys and keyrings, passphrases, as well as session keys. +It is designed with gomobile users in mind, so that they can use the full power of the library, +without having to rely on a further wrapper. + +This version comes with some design improvements, in particular the introduction of keys +### Security +- Dropped the use of strings for secrets +- New key checking functions +- Clear memory after use, in an attempt to reduce leftover secrets in RAM. +- Improved testing, in this and the underlying crypto library + +### Fixed +- `KeyRing`s can now only be unencrypted, removing the problem of mixed encrypted/decrypted keyring, that caused keys not to be recognised. +- Explicit key decryption and encryption. +- Underlying crypto library update. +- Underlying MIME library update. +- Fixed ECC critical bugs. +- Removed gopenpgp/pmcrypto object as it could create multiple globals. Methods are now static on the crypto object. + +### Removed +- `Signature` struct +- `Signature#KeyRing` function +- `Signature#IsBy` function +- `pmKeyObject` struct +- `encodedLength` function, internal and and unused +- `EncryptCore` is now internal. +- `RandomTokenWith`, `RandomToken` now takes a size +- In the `KeyRing` struct: + - `KeyRing#GetEntities`, entities are handled by the lib + - `KeyRing#GetSigningEntity`, has been made internal + - `KeyRing#Unlock`, the unlocking functionalities are on now on the key object + - `BuildKeyRingNoError`, `BuildKeyRingArmored`, `BuildKeyRing` use `NewKey` or `NewKeyFromArmored` and handle errors +then join them into KeyRings. + - `ReadKeyRing`, `ReadArmoredKeyRing`, use `NewKeyFromArmoredReader` or `NewKeyFromReader`. + - `UnmarshalJSON`, the interface to unmarshal JSON is not relevant to this library. + + +### Added +- `Key` struct, to store, import (unserialize) and export (serialize) keys. +```go +// Key contains a single private or public key +type Key struct { + // PGP entities in this keyring. + entity *openpgp.Entity +} + +// With the functions +NewKeyFromArmoredReader(r io.Reader) (key *Key, err error) +NewKeyFromReader(r io.Reader) (key *Key, err error) +NewKey(binKeys []byte) (key *Key, err error) +NewKeyFromArmored(armored string) (key *Key, err error) +GenerateKey(name, email string, keyType string, bits int) (*Key, error) +GenerateRSAKeyWithPrimes(name, email string, bits int, primeone, primetwo, primethree, primefour []byte) (*Key, error) +(key *Key) Clone() (*Key, error) +(key *Key) Lock(passphrase []byte) (*Key, error) +(key *Key) Unlock(passphrase []byte) (*Key, error) +(key *Key) Serialize() ([]byte, error) +(key *Key) Armor() (string, error) +(key *Key) GetArmoredPublicKey() (s string, err error) +(key *Key) GetPublicKey() (b []byte, err error) +(key *Key) IsExpired() bool +(key *Key) IsPrivate() bool +(key *Key) IsLocked() (bool, error) +(key *Key) IsUnlocked() (bool, error) +(key *Key) Check() (bool, error) +(key *Key) PrintFingerprints() +(key *Key) GetHexKeyID() string +(key *Key) GetKeyID() uint64 +(key *Key) GetFingerprint() string +(key *Key) ClearPrivateParams() (ok bool) +``` + +- In the `KeyRing` object: +```go +NewKeyRing(key *Key) (*KeyRing, error) +(keyRing *KeyRing) AddKey(key *Key) error +(keyRing *KeyRing) GetKeys() []*Key +(keyRing *KeyRing) GetKey(n int) (*Key, error) +(keyRing *KeyRing) CountEntities() int +(keyRing *KeyRing) CountDecryptionEntities() int +(keyRing *KeyRing) GetIdentities() []*Identity +(keyRing *KeyRing) FirstKey() (*KeyRing, error) +(keyRing *KeyRing) Clone() (*KeyRing, error) +(keyRing *KeyRing) ClearPrivateParams() +``` + +- `PlainMessage` struct, to store un-encrypted messages +```go +// PlainMessage stores a plain text / unencrypted message. +type PlainMessage struct { + // The content of the message + Data []byte + // if the content is text or binary + TextType bool +} + +// With the functions +NewPlainMessage(data []byte) *PlainMessage +NewPlainMessageFromString(text string) *PlainMessage +(msg *PlainMessage) GetBinary() +(msg *PlainMessage) GetString() +(msg *PlainMessage) GetBase64() +(msg *PlainMessage) NewReader() +(msg *PlainMessage) IsText() +(msg *PlainMessage) IsBinary() +``` + +- `PGPMessage` struct, to store encrypted PGP messages +```go +// PGPMessage stores a PGP-encrypted message. +type PGPMessage struct { + // The content of the message + Data []byte +} + +// With the functions +NewPGPMessage(data []byte) *PGPMessage +NewPGPMessageFromArmored(armored string) (*PGPMessage, error) +(msg *PGPMessage) GetBinary() []byte +(msg *PGPMessage) NewReader() io.Reader +(msg *PGPMessage) GetArmored() (string, error) +(msg *PGPMessage) SeparateKeyAndData(estimatedLength, garbageCollector int) (outSplit *PGPSplitMessage, err error) +``` + +- `PGPSignature` struct, to store detached PGP signatures +```go +// PGPSignature stores a PGP-encoded detached signature. +type PGPSignature struct { + // The content of the message + Data []byte +} + +// With the functions +NewPGPSignature(data []byte) *PGPSignature +NewPGPSignatureFromArmored(armored string) (*PGPSignature, error) +(msg *PGPSignature) GetBinary() []byte +(msg *PGPSignature) GetArmored() (string, error) +``` + +- `SignatureVerificationError` struct, to separate signature verification errors from decryption errors +```go +// SignatureVerificationError is returned from Decrypt and VerifyDetached functions when signature verification fails +type SignatureVerificationError struct { + Status int + Message string +} +``` + +### Changed +- `IsKeyExpiredBin` has been renamed to `IsKeyExpired` +- `IsKeyExpired` has been renamed to `IsArmoredKeyExpired` +- `CheckKey` has been renamed to `PrintFingerprints` +- `KeyRing#ArmoredPublicKeyString` has been renamed to `KeyRing#GetArmoredPublicKey` +- `KeyRing#KeyIds` has been renamed to `KeyRing#GetKeyIDs` +- `GetTimeUnix` was renamed to `GetUnixTime` + +- `EncryptedSplit` has been changed to `PGPSplitMessage` +```go +models.EncryptedSplit struct { + DataPacket []byte + KeyPacket []byte + Algo string +} +// Is now +crypto.PGPSplitMessage struct { + DataPacket []byte + KeyPacket []byte +} + +// With the functions +NewPGPSplitMessage(keyPacket []byte, dataPacket []byte) *PGPSplitMessage +NewPGPSplitMessageFromArmored(encrypted string) (*PGPSplitMessage, error) +(msg *PGPSplitMessage) GetBinaryDataPacket() []byte +(msg *PGPSplitMessage) GetBinaryKeyPacket() []byte +(msg *PGPSplitMessage) GetBinary() []byte +(msg *PGPSplitMessage) GetArmored() (string, error) +``` + +- `DecryptSignedVerify` has been changed to `ExplicitVerifyMessage` +```go +models.DecryptSignedVerify struct { + //clear text + Plaintext string + //bitmask verify status : 0 + Verify int + //error message if verify failed + Message string +} +// Is now +// ExplicitVerifyMessage contains explicitely the signature verification error, for gomobile users +type ExplicitVerifyMessage struct { + Message *crypto.PlainMessage + SignatureVerificationError *crypto.SignatureVerificationError +} +// With the new helper +DecryptExplicitVerify (pgpMessage *crypto.PGPMessage, privateKeyRing, publicKeyRing *crypto.KeyRing, verifyTime int64) (*ExplicitVerifyMessage, error) +``` + +- `SignedString` has been changed to `ClearTextMessage` +```go +// SignedString wraps string with Signature +type SignedString struct { + String string + Signed *Signature +} +// Is now +// ClearTextMessage, split signed clear text message container +type ClearTextMessage struct { + Data []byte + Signature []byte +} + +// With the functions +NewClearTextMessage(data []byte, signature []byte) *ClearTextMessage +NewClearTextMessageFromArmored(signedMessage string) (*ClearTextMessage, error) +(msg *ClearTextMessage) GetBinary() []byte +(msg *ClearTextMessage) GetString() string +(msg *ClearTextMessage) GetBinarySignature() []byte +(msg *ClearTextMessage) GetArmored() (string, error) +``` +- `SymmetricKey` has been renamed to `SessionKey` +```go +// SessionKey stores a decrypted session key. +type SessionKey struct { + // The decrypted binary session key. + Key []byte + // The symmetric encryption algorithm used with this key. + Algo string +} + +// With the functions +NewSessionKeyFromToken(token []byte, algo string) *SessionKey +GenerateSessionKey() (*SessionKey, error) +GenerateSessionKeyAlgo(algo string) (sk *SessionKey, err error) +(sk *SessionKey) GetCipherFunc() packet.CipherFunction +(sk *SessionKey) GetBase64Key() string +(sk *SessionKey) Encrypt(message *PlainMessage) ([]byte, error) +(sk *SessionKey) Decrypt(dataPacket []byte) (*PlainMessage, error) +(sk *SessionKey) Clear() (ok bool) +``` + +- `ReadClearSignedMessage` moved to crypto package and renamed to `NewClearTextMessageFromArmored`. Changed to return `ClearTextMessage`. +```go +ReadClearSignedMessage(signedMessage string) (string, error) +// Is now +NewClearTextMessageFromArmored(signedMessage string) (*ClearTextMessage, error) + +// In addition, were added: +NewClearTextMessage(data []byte, signature []byte) *ClearTextMessage +(msg *ClearTextMessage) GetBinary() []byte +(msg *ClearTextMessage) GetString() string +(msg *ClearTextMessage) GetBinarySignature() []byte +(msg *ClearTextMessage) GetArmored() (string, error) + +// As helpers were added: +SignCleartextMessageArmored(privateKey string, passphrase []byte, text string) (string, error) +VerifyCleartextMessageArmored(publicKey, armored string, verifyTime int64) (string, error) +SignCleartextMessage(keyRing *crypto.KeyRing, text string) (string, error) +VerifyCleartextMessage(keyRing *crypto.KeyRing, armored string, verifyTime int64) (string, error) +``` + +- `EncryptAttachment`'s parameters are changed to messages. +```go +(pm *PmCrypto) EncryptAttachment(plainData []byte, fileName string, publicKey *KeyRing) (*models.EncryptedSplit, error) +// Is now +(keyRing *KeyRing) EncryptAttachment(message *PlainMessage, fileName string) (*PGPSplitMessage, error) + +// As a helper was added: +EncryptSignAttachment(publicKey, privateKey string, passphrase []byte, fileName string, plainData []byte) (keyPacket, dataPacket, signature []byte, err error) +``` + +- `DecryptAttachment` has been moved to KeyRing struct (like `EncryptAttachment`) +```go +(pm *PmCrypto) DecryptAttachment(keyPacket []byte, dataPacket []byte, kr *KeyRing, passphrase string) ([]byte, error) +// Is now +(keyRing *KeyRing) DecryptAttachment(message *PGPSplitMessage) (*PlainMessage, error) + +// As a helper was added: +DecryptVerifyAttachment(publicKey, privateKey string, passphrase, keyPacket, dataPacket []byte, armoredSignature string) (plainData []byte, err error) +``` + +- `EncryptAttachmentLowMemory` was renamed to `NewLowMemoryAttachmentProcessor`. +```go +(pm *PmCrypto) EncryptAttachmentLowMemory(estimatedSize int, fileName string, publicKey *KeyRing) (*AttachmentProcessor, error) +// Is now +(keyRing *KeyRing) NewLowMemoryAttachmentProcessor(estimatedSize int, fileName string) (*AttachmentProcessor, error) +``` + +- `SplitArmor` was renamed to `NewPGPSplitMessageFromArmored` and the model changed. +```go +SplitArmor(encrypted string) (*models.EncryptedSplit, error) +// Is now +NewPGPSplitMessageFromArmored(encrypted string) (*PGPSplitMessage, error) +``` + +- `DecryptAttKey` was renamed to `DecryptSessionKey` and the parameter keypacket changed to `[]byte` as it's binary, not armored. +```go +DecryptAttKey(kr *KeyRing, keyPacket string) (key *SymmetricKey, err error): +// Is now +(keyRing *KeyRing) DecryptSessionKey(keyPacket []byte) (*SessionKey, error) +``` + +- `SetKey` has been renamed to `EncryptSessionKey`, and the keypacket return value changed to `[]byte`. +```go +SetKey(kr *KeyRing, symKey *SymmetricKey) (packets string, err error): +// Is now +(keyRing *KeyRing) EncryptSessionKey(sessionSplit *SessionKey) ([]byte, error) +``` + +- `SeparateKeyAndData` has been split in two different function, as it did not only separate the data, but when provided a KeyRing decrypted the session key too. +```go +SeparateKeyAndData(kr *KeyRing, r io.Reader, estimatedLength int, garbageCollector int) (outSplit *models.EncryptedSplit, err error): + +// Is now the conjunction of the following function: +// To separate key and data +(msg *PGPMessage) SeparateKeyAndData(estimatedLength, garbageCollector int) (outSplit *PGPSplitMessage, err error) +// To decrypt the SessionKey +(keyRing *KeyRing) DecryptSessionKey(keyPacket []byte) (*SessionKey, error) +``` + +- `EncryptSymmetric` has been changed, now the procedure is split in two parts: `Encrypt` and `SeparateKeyAndData` +```go +(kr *KeyRing) EncryptSymmetric(textToEncrypt string, canonicalizeText bool) (outSplit *models.EncryptedSplit, err error): +// Is now the conjunction of the following function: +// To encrypt +(keyRing *KeyRing) Encrypt(message *PlainMessage, privateKey *KeyRing) (*PGPMessage, error) +// To separate key and data +(msg *PGPMessage) SeparateKeyAndData(estimatedLength, garbageCollector int) (outSplit *PGPSplitMessage, err error) +``` + +- `GenerateKey`'s signature has been altered: + - It now returns a `Key` struct + - `userName` and `domain` are now joined in `email`, the `name` parameter was added (To emulate the old behaviour `name = email = userName + "@" + domain`). +```go +(pm *PmCrypto) GenerateKey(userName, domain, passphrase, keyType string, bits int) (string, error) : +// Is now +GenerateKey(name, email string, keyType string, bits int) (*Key, error) + +// As a helper was added: +GenerateKey(name, email string, passphrase []byte, keyType string, bits int) (string, error) +``` + +- `GenerateRSAKeyWithPrimes`'s signature has been altered: + - It now returns a `Key` struct + - `userName` and `domain` are now joined in `email`, the `name` parameter was added (To emulate the old behaviour `name = email = userName + "@" + domain`). +```go +(pm *PmCrypto) GenerateRSAKeyWithPrimes(userName, domain, passphrase, keyType string, bits int, prime1, prime2, prime3, prime4 []byte) (string, error): +GenerateRSAKeyWithPrimes(name, email string, bits int, primeone, primetwo, primethree, primefour []byte,) (*Key, error) +``` + +- `Encrypt`, `EncryptArmored`, `EncryptString`, `EncryptMessage` functions have been changed to return and accept messages. +```go +(kr *KeyRing) Encrypt(w io.Writer, sign *KeyRing, filename string, canonicalizeText bool) (io.WriteCloser, error) +// Is now +(keyRing *KeyRing) Encrypt(message *PlainMessage, privateKey *KeyRing) (*PGPMessage, error) + +// As a helpers were added: +EncryptMessageArmored(publicKey, plaintext string) (ciphertext string, err error) +EncryptSignMessageArmored(publicKey, privateKey string, passphrase []byte, plaintext string) (ciphertext string, err error) { +``` + +- `Decrypt`, `DecryptArmored`, `DecryptString`, `DecryptMessage`, `DecryptMessageVerify`, and `DecryptMessageStringKey` functions have been changed to return and accept messages (Same as Encrypt*). +If signature verification fails they will return a SignatureVerificationError. +```go +(kr *KeyRing) DecryptString(encrypted string) (SignedString, error) +// Is now +(keyRing *KeyRing) Decrypt(message *PGPMessage, verifyKey *KeyRing, verifyTime int64) (*PlainMessage, error) + +// As a helpers were added: +DecryptMessageArmored(privateKey string, passphrase []byte, ciphertext string) (plaintext string, err error) +DecryptVerifyMessageArmored(publicKey, privateKey string, passphrase []byte, ciphertext string) (plaintext string, err error) +DecryptExplicitVerify(pgpMessage *crypto.PGPMessage, privateKeyRing, publicKeyRing *crypto.KeyRing, verifyTime int64) (*ExplicitVerifyMessage, error) { +``` +- `DecryptStringIfNeeded` has been replaced with `IsPGPMessage` + `Decrypt*`. +```go +(kr *KeyRing) DecryptStringIfNeeded(data string) (decrypted string, err error) +// Is now the conjunction of the following function: +// To check if the data is a PGP message +IsPGPMessage(data string) bool +// To decrypt +(keyRing *KeyRing) Decrypt(message *PGPMessage, verifyKey *KeyRing, verifyTime int64) (*PlainMessage, error) +``` + +- `SignString` and `DetachedSign` have been replaced by signing methods. +```go +(kr *KeyRing) SignString(message string, canonicalizeText bool) (signed string, err error) +(kr *KeyRing) DetachedSign(w io.Writer, toSign io.Reader, canonicalizeText bool, armored bool) +// Are now +(keyRing *KeyRing) SignDetached(message *PlainMessage) (*PGPSignature, error) +``` + +- `VerifyString` has been altered in the same way as as signing. +Returns SignatureVerificationError if the verification fails. +```go +(kr *KeyRing) VerifyString(message, signature string, sign *KeyRing) (err error) +// Is now +(keyRing *KeyRing) VerifyDetached(message *PlainMessage, signature *PGPSignature, verifyTime int64) error +``` + +- `EncryptMessageWithPassword` uses AES-256 instead of AES-128, and has a new signature. +```go +(pm *PmCrypto) EncryptMessageWithPassword(plaintext string, password string) (string, error) +// Is now +EncryptMessageWithPassword(message *PlainMessage, password []byte) (*PGPMessage, error) + +// As a helper was added: +EncryptMessageWithPassword(password []byte, plaintext string) (ciphertext string, err error) +``` + +- `DecryptMessageWithPassword` accepts all symmetric algorithms known to the lib, and has a new signature +```go +(pm *PmCrypto) DecryptMessageWithPassword(encrypted string, password string) (string, error) +// Is now +DecryptMessageWithPassword(message *PGPMessage, password []byte) (*PlainMessage, error) + +// As a helper was added: +DecryptMessageWithPassword(password []byte, ciphertext string) (plaintext string, err error) +``` + +- `DecryptMIMEMessage` was moved to `KeyRing`, and the parameters transformed to messages +```go +(pm *PmCrypto) DecryptMIMEMessage(encryptedText string, verifierKey *KeyRing, privateKeyRing *KeyRing, passphrase string, callbacks MIMECallbacks, verifyTime int64): +// Is now +(keyRing *KeyRing) DecryptMIMEMessage(message *PGPMessage, verifyKey *KeyRing, callbacks MIMECallbacks, verifyTime int64) +``` + +- `RandomToken` now takes a size +```go +(pm *PmCrypto) RandomToken() ([]byte, error) +// Is now +RandomToken(size int) ([]byte, error) +``` + +- `GetSessionFromKeyPacket` was changed to `DecryptSessionKey`. +```go +(pm *PmCrypto) GetSessionFromKeyPacket(keyPackage []byte, privateKey *KeyRing, passphrase string) (*SymmetricKey, error) +// Is now +(keyRing *KeyRing) DecryptSessionKey(keyPacket []byte) (*SessionKey, error) +``` + +- `KeyPacketWithPublicKey` and `KeyPacketWithPublicKeyBin` have been merged to `EncryptSessionKey`. +```go +(pm *PmCrypto) KeyPacketWithPublicKey(sessionSplit *SymmetricKey, publicKey string) ([]byte, error) +(pm *PmCrypto) KeyPacketWithPublicKeyBin(sessionSplit *SymmetricKey, publicKey []byte) ([]byte, error) +(keyRing *KeyRing) EncryptSessionKey(sk *SessionKey) ([]byte, error) +``` + +- `GetSessionFromSymmetricPacket` was renamed to `DecryptSessionKeyWithPassword`. +```go +(pm *PmCrypto) GetSessionFromSymmetricPacket(keyPackage []byte, password string) (*SymmetricKey, error) +// Is now +DecryptSessionKeyWithPassword(keyPacket, password []byte) (*SessionKey, error) +``` + +- `SymmetricKeyPacketWithPassword` has been renamed to `EncryptSessionKeyWithPassword` +```go +(pm *PmCrypto) SymmetricKeyPacketWithPassword(sessionSplit *SymmetricKey, password string) ([]byte, error): +EncryptSessionKeyWithPassword(sk *SessionKey, password []byte]) ([]byte, error) +``` + +- `SignTextDetached` and `SignBinDetached` have been changed to `SignDetached` +```go +(pm *PmCrypto) SignTextDetached(plaintext string, privateKey *KeyRing, passphrase string, trim bool) (string, error) +(pm *PmCrypto) SignBinDetached(plainData []byte, privateKey *KeyRing, passphrase string) (string, error) +// Are now +(keyRing *KeyRing) SignDetached(message *PlainMessage) (*PGPSignature, error) + +// As helpers were added: +SignCleartextMessage(keyRing *crypto.KeyRing, text string) (string, error) +SignCleartextMessageArmored(privateKey string, passphrase []byte, text string) (string, error) +``` + +- `VerifyTextSignDetachedBinKey` and `VerifyBinSignDetachedBinKey` have been changed to `Verify`. +```go +(pm *PmCrypto) VerifyTextSignDetachedBinKey(signature string, plaintext string, publicKey *KeyRing, verifyTime int64) (bool, error): +(pm *PmCrypto) VerifyBinSignDetachedBinKey(signature string, plainData []byte, publicKey *KeyRing, verifyTime int64) (bool, error) +// Are now +(keyRing *KeyRing) VerifyDetached(message *PlainMessage, signature *PGPSignature, verifyTime int64) error + +// As helpers were added: +VerifyCleartextMessage(keyRing *crypto.KeyRing, armored string, verifyTime int64) (string, error) +VerifyCleartextMessageArmored(publicKey, armored string, verifyTime int64) (string, error) +``` + +## [1.0.0] - 2019-05-15 +Initial release, opensourcing of the internal library `PMCrypto`, and subsequent renaming to `gopenpgp` \ No newline at end of file diff --git a/Changelog.md b/Changelog.md deleted file mode 100644 index d57db6d..0000000 --- a/Changelog.md +++ /dev/null @@ -1,17 +0,0 @@ -# ProtonMail Bridge Changelog - -Changelog [format](http://keepachangelog.com/en/1.0.0/) - -## [1.0.0] - 2019-03-07 -* `master` refactor of master contains all changes from `oldMaster` - -### Added -* `FirstKeyID` into `KeyRing` object to be able match salts - -### Changed -* If statement re-factor following linter recommendations -* Constants rename following linter recomendations -* Comments following linter recomendations (not complete) -* Update the crypto and mime dependencies -* Error handling in `GetSessionFromKeyPaket` - diff --git a/ProposalChanges.md b/ProposalChanges.md deleted file mode 100644 index 82f9259..0000000 --- a/ProposalChanges.md +++ /dev/null @@ -1,474 +0,0 @@ -# Model changes -## Modified -### EncryptedSplit -``` -models.EncryptedSplit struct { - DataPacket []byte - KeyPacket []byte - Algo string -} -``` -is now -``` -crypto.PGPSplitMessage struct { - DataPacket []byte - KeyPacket []byte -} -``` - -### DecryptSignedVerify -``` -models.DecryptSignedVerify struct { - //clear text - Plaintext string - //bitmask verify status : 0 - Verify int - //error message if verify failed - Message string -} -``` -is now -``` -// PlainMessage stores an unencrypted text message. -crypto.PlainMessage struct { - // The content of the message - Text string - // If the decoded message was correctly signed. See constants.SIGNATURE* for all values. - Verified int -} -``` - -### SignedString -``` -// SignedString wraps string with Signature -type SignedString struct { - String string - Signed *Signature -} -``` -is now -``` -// ClearTextMessage, split signed clear text message container -type ClearTextMessage struct { - Data []byte - Signature []byte -} - -``` - -## Dropped -### Signature -``` -type Signature struct { - md *openpgp.MessageDetails -} -``` - -### pmKeyObject -``` -type pmKeyObject struct { - ID string - Version int - Flags int - Fingerprint string - PublicKey string `json:",omitempty"` - PrivateKey string - Primary int -} -``` - -## New -### PGPMessage -``` -// PGPMessage stores a PGP-encrypted message. -type PGPMessage struct { - // The content of the message - Data []byte -} -``` - -### PGPSignature -``` -// PGPSignature stores a PGP-encoded detached signature. -type PGPSignature struct { - // The content of the message - Data []byte -} -``` - -### SignatureVerificationError -``` -// SignatureVerificationError is returned from Decrypt and VerifyDetached functions when signature verification fails -type SignatureVerificationError struct { - Status int - Message string -} -``` - - -# API changes -## armor.go -### ReadClearSignedMessage -Moved to crypto package. Changed to return ClearTextMessage. -``` -ReadClearSignedMessage(signedMessage string) (string, error): -* NewClearTextMessageFromArmored(signedMessage string) (*ClearTextMessage, error) -``` -In addition, were added: -``` -* NewClearTextMessage(data []byte, signature []byte) *ClearTextMessage -* (msg *ClearTextMessage) GetBinary() []byte -* (msg *ClearTextMessage) GetString() string -* (msg *ClearTextMessage) GetBinarySignature() []byte -* (msg *ClearTextMessage) GetArmored() (string, error) -``` - -## attachment.go -### AttachmentProcessor -No change. - -### EncryptAttachment -Change encryption parameters to messages: either contextual signature with helper or using messages. -``` -(pm *PmCrypto) EncryptAttachment(plainData []byte, fileName string, publicKey *KeyRing) (*models.EncryptedSplit, error): -* (helper) EncryptSignAttachment(publicKey, privateKey, passphrase, fileName string, plainData []byte) (keyPacket, dataPacket, signature []byte, err error) -* (keyRing *KeyRing) EncryptAttachment(message *PlainMessage, fileName string) (*PGPSplitMessage, error) -``` - -### EncryptAttachmentLowMemory -Renamed. -``` -(pm *PmCrypto) EncryptAttachmentLowMemory(estimatedSize int, fileName string, publicKey *KeyRing) (*AttachmentProcessor, error): -* (keyRing *KeyRing) NewLowMemoryAttachmentProcessor(estimatedSize int, fileName string) (*AttachmentProcessor, error) -``` - -### SplitArmor -Renamed, changed model. -``` -SplitArmor(encrypted string) (*models.EncryptedSplit, error): -* NewPGPSplitMessageFromArmored(encrypted string) (*PGPSplitMessage, error) -``` - -### DecryptAttachment -Same as `EncryptAttachment`. -``` -(pm *PmCrypto) DecryptAttachment(keyPacket []byte, dataPacket []byte, kr *KeyRing, passphrase string) ([]byte, error): -* (helper) DecryptVerifyAttachment(publicKey, privateKey, passphrase string, keyPacket, dataPacket []byte, armoredSignature string) (plainData []byte, err error) -* (keyRing *KeyRing) DecryptAttachment(message *PGPSplitMessage) (*PlainMessage, error) -``` - -## key.go -`SymmetricKey` model and functions have been moved to symmetrickey.go - -### DecryptAttKey -Renamed, change to `[]byte` as it's a binary keypacket. -``` -DecryptAttKey(kr *KeyRing, keyPacket string) (key *SymmetricKey, err error): -* (keyRing *KeyRing) DecryptSessionKey(keyPacket []byte) (*SymmetricKey, error) -``` - -### SeparateKeyAndData -This function has been split in two, as it **did not** only separate the data, but when provided a KeyRing decrypt the session key too. -``` -SeparateKeyAndData(kr *KeyRing, r io.Reader, estimatedLength int, garbageCollector int) (outSplit *models.EncryptedSplit, err error): -* (for separating key and data) (msg *PGPMessage) SeparateKeyAndData(estimatedLength, garbageCollector int) (outSplit *PGPSplitMessage, err error) -* (for decrypting SessionKey) (keyRing *KeyRing) DecryptSessionKey(keyPacket []byte) (*SymmetricKey, error) -``` - -### encodedLength -Dropped as already present in `SeparateKeyAndData` and unused. - -### SetKey -Renamed, change to `[]byte` as it's a binary keypacket. -``` -SetKey(kr *KeyRing, symKey *SymmetricKey) (packets string, err error): -* (keyRing *KeyRing) EncryptSessionKey(sessionSplit *SymmetricKey) ([]byte, error) -``` - -### IsKeyExpiredBin -Renamed. -``` -(pm *PmCrypto) IsKeyExpiredBin(publicKey []byte) (bool, error): -* IsKeyExpired(publicKey []byte) (bool, error) -``` - -### IsKeyExpired -Renamed. -``` -(pm *PmCrypto) IsKeyExpired(publicKey string) (bool, error): -* IsArmoredKeyExpired(publicKey string) (bool, error) -``` - -### GenerateRSAKeyWithPrimes -`userName` and `domain` joined in `email`. -Added `name` parameter. -To emulate the old behaviour `name = email = userName + "@" + domain`. -``` -(pm *PmCrypto) GenerateRSAKeyWithPrimes(userName, domain, passphrase, keyType string, bits int, prime1, prime2, prime3, prime4 []byte) (string, error): -* GenerateRSAKeyWithPrimes(name, email, passphrase, keyType string, bits int, prime1, prime2, prime3, prime4 []byte) (string, error): -``` - -### GenerateKey -`userName` and `domain` joined in `email`. -Added `name` parameter. -To emulate the old behaviour `name = email = userName + "@" + domain`. -``` -(pm *PmCrypto) GenerateKey(userName, domain, passphrase, keyType string, bits int) (string, error) : -* GenerateKey(name, email, passphrase, keyType string, bits int) (string, error): -``` - -### UpdatePrivateKeyPassphrase -No change. - -### CheckKey -Renamed. -``` -(pm *PmCrypto) CheckKey(pubKey string) (string, error): -* PrintFingerprints(pubKey string) (string, error) -``` - -## keyring.go -### Signature.KeyRing -Dropped with signature. - -### Signature.IsBy -Dropped with signature. - -### GetEntities -No change. - -### GetSigningEntity -KeyRings must be already unlocked when provided to encrypt/decrypt/sign/verify functions. -``` -(kr *KeyRing) GetSigningEntity(passphrase string) *openpgp.Entity: -* (keyRing *KeyRing) GetSigningEntity() (*openpgp.Entity, error) -``` - -### Encrypt, EncryptArmored, EncryptString -This function has been divided in different sub-functions and wrappers have been provided for the key unlock and message models. -``` -(kr *KeyRing) Encrypt(w io.Writer, sign *KeyRing, filename string, canonicalizeText bool) (io.WriteCloser, error): -* (if binary data) (keyRing *KeyRing) Encrypt(message *PlainMessage, privateKey *KeyRing) (*PGPMessage, error) -* (if plain text, wrapped) (helper) EncryptMessageArmored(publicKey, plaintext string) (ciphertext string, err error) -* (if plain text, wrapped, signed) (helper) EncryptSignMessageArmored(publicKey, privateKey, passphrase, plaintext string) (ciphertext string, err error) -``` -### EncryptCore -Made an internal function. - -### EncryptSymmetric -Dropped, now the procedure is split in two parts. -``` -(kr *KeyRing) EncryptSymmetric(textToEncrypt string, canonicalizeText bool) (outSplit *models.EncryptedSplit, err error): -* (for encrypting) (keyRing *KeyRing) Encrypt* -* (for splitting) (msg *PGPMessage) SeparateKeyAndData(estimatedLength, garbageCollector int) (outSplit *PGPSplitMessage, err error) -* (alternative) (keyRing *KeyRing) EncryptAttachment(message *PlainMessage, fileName string) (*PGPSplitMessage, error) -``` - -### DecryptString, Decrypt, DecryptArmored -Same as Encrypt*. If signature verification fails it will return a SignatureVerificationError. -``` -(kr *KeyRing) DecryptString(encrypted string) (SignedString, error): -* (if binary data) func (keyRing *KeyRing) Decrypt(message *PGPMessage, verifyKey *KeyRing, verifyTime int64) (*PlainMessage, error) -* (if plain text, wrapped) (helper) DecryptMessageArmored(privateKey, passphrase, ciphertext string) (plaintext string, err error) -* (if plain text, wrapped, verified) (helper) DecryptVerifyMessageArmored(publicKey, privateKey, passphrase, ciphertext string) (plaintext string, err error) -``` - -### DecryptStringIfNeeded -Replaced with `IsPGPMessage` + `Decrypt*`. -``` -(kr *KeyRing) DecryptStringIfNeeded(data string) (decrypted string, err error): -* IsPGPMessage(data string) bool -``` - -### SignString, DetachedSign -Replaced by signing methods. -``` -(kr *KeyRing) SignString(message string, canonicalizeText bool) (signed string, err error): -(kr *KeyRing) DetachedSign(w io.Writer, toSign io.Reader, canonicalizeText bool, armored bool): -* (keyRing *KeyRing) SignDetached(message *PlainMessage) (*PGPSignature, error) -``` - -### VerifyString -Same as signing. Returns SignatureVerificationError if the verification fails. -``` -(kr *KeyRing) VerifyString(message, signature string, sign *KeyRing) (err error): -* (to verify) (keyRing *KeyRing) VerifyDetached(message *PlainMessage, signature *PGPSignature, verifyTime int64) error -``` - -### Unlock -No change. Added: -``` -(keyRing *KeyRing) UnlockWithPassphrase(passphrase string) error -``` - -### WriteArmoredPublicKey -No change. - -### ArmoredPublicKeyString -Renamed. -``` -(kr *KeyRing) ArmoredPublicKeyString() (s string, err error): -* (keyRing *KeyRing) GetArmoredPublicKey() (s string, err error) -``` - -### BuildKeyRing -No change. - -### BuildKeyRingNoError -No change. - -### BuildKeyRingArmored -No change. - -### UnmarshalJSON -Dropped. - -### Identities -No change - -### KeyIds -No change. - -### ReadArmoredKeyRing -No change. - -### ReadKeyRing -No change. - -### FilterExpiredKeys -No change. - -## message.go -Many functions are duplicates of keyring.go - -### EncryptMessage -See Encrypt* -``` -(pm *PmCrypto) EncryptMessage(plaintext string, publicKey *KeyRing, privateKey *KeyRing, passphrase string, trim bool) (string, error): -* (if binary data) (keyRing *KeyRing) Encrypt(message *PlainMessage, privateKey *KeyRing) (*PGPMessage, error) -* (if plain text, wrapped) (helper) EncryptMessageArmored(publicKey, plaintext string) (ciphertext string, err error) -* (if plain text, wrapped, signed) (helper) EncryptSignMessageArmored(publicKey, privateKey, passphrase, plaintext string) (ciphertext string, err error) -``` - -### DecryptMessage, DecryptMessageVerify, DecryptMessageStringKey -See Decrypt* -``` -(pm *PmCrypto) DecryptMessage(encryptedText string, privateKey *KeyRing, passphrase string) (string, error): -(pm *PmCrypto) DecryptMessageStringKey(encryptedText string, privateKey string, passphrase string) (string, error): -(pm *PmCrypto) DecryptMessageVerify(encryptedText string, verifierKey *KeyRing, privateKeyRing *KeyRing, passphrase string, verifyTime int64) (*models.DecryptSignedVerify, error) : -* (if binary data) (keyRing *KeyRing) Decrypt(message *PGPMessage, verifyKey *KeyRing, verifyTime int64) (*PlainMessage, error) -* (if plain text, wrapped) (helper) DecryptMessageArmored(privateKey, passphrase, ciphertext string) (plaintext string, err error) -* (if plain text, wrapped, verified) (helper) DecryptVerifyMessageArmored(publicKey, privateKey, passphrase, ciphertext string) (plaintext string, err error) -``` - -### EncryptMessageWithPassword -The function has been renamed and moved to `SymmetricKey` to allow more encryption modes. Previously AES-128 (! not 256 as stated) was used. -``` -(pm *PmCrypto) EncryptMessageWithPassword(plaintext string, password string) (string, error): -* (if binary data) (symmetricKey *SymmetricKey) Encrypt(message *PlainMessage) (*PGPMessage, error) -* (if plain text, wrapped) (helper) EncryptMessageWithToken(token, plaintext string) (ciphertext string, err error) -* (if plain text, wrapped) (helper) EncryptMessageWithTokenAlgo(token, plaintext, algo string) (ciphertext string, err error) -``` - -### DecryptMessageWithPassword -See `EncryptMessageWithPassword`. -``` -(pm *PmCrypto) DecryptMessageWithPassword(encrypted string, password string) (string, error): -* (if binary data) (symmetricKey *SymmetricKey) Decrypt(message *PGPMessage) (*PlainMessage, error) -* (if plain text, wrapped, for all ciphers) (helper) DecryptMessageWithToken(token, ciphertext string) (plaintext string, err error) -``` - -## mime.go - -### DecryptMIMEMessage -Moved to `KeyRing`. -``` -(pm *PmCrypto) DecryptMIMEMessage(encryptedText string, verifierKey *KeyRing, privateKeyRing *KeyRing, passphrase string, callbacks MIMECallbacks, verifyTime int64): -* (keyRing *KeyRing) DecryptMIMEMessage(message *PGPMessage, verifyKey *KeyRing, callbacks MIMECallbacks, verifyTime int64) -``` - -## session.go -### RandomToken -No change. - -### RandomTokenWith -Renamed. -``` -(pm *PmCrypto) RandomTokenWith(size int) ([]byte, error): -* RandomTokenSize(size int) ([]byte, error) -``` - -### GetSessionFromKeyPacket -Dropped, use now `DecryptSessionKey`. -``` -(pm *PmCrypto) GetSessionFromKeyPacket(keyPackage []byte, privateKey *KeyRing, passphrase string) (*SymmetricKey, error): -* (keyRing *KeyRing) DecryptSessionKey(keyPacket []byte) (*SymmetricKey, error) -``` - -### KeyPacketWithPublicKey, KeyPacketWithPublicKeyBin -Dropped, use now `EncryptSessionKey`. -``` -(pm *PmCrypto) KeyPacketWithPublicKey(sessionSplit *SymmetricKey, publicKey string) ([]byte, error): -(pm *PmCrypto) KeyPacketWithPublicKeyBin(sessionSplit *SymmetricKey, publicKey []byte) ([]byte, error): -* (keyRing *KeyRing) EncryptSessionKey(sessionSplit *SymmetricKey) ([]byte, error) -``` - -### GetSessionFromSymmetricPacket -Renamed, moved to `SymmetricKey`. -``` -(pm *PmCrypto) GetSessionFromSymmetricPacket(keyPackage []byte, password string) (*SymmetricKey, error): -* NewSymmetricKeyFromKeyPacket(keyPacket []byte, password string) (*SymmetricKey, error) -``` - -### SymmetricKeyPacketWithPassword -Renamed, moved to `SymmetricKey`. -``` -(pm *PmCrypto) SymmetricKeyPacketWithPassword(sessionSplit *SymmetricKey, password string) ([]byte, error): -* (symmetricKey *SymmetricKey) EncryptToKeyPacket(password string) ([]byte, error) -``` - -## sign_detached.go - -### SignTextDetached -Moved to `KeyRing`, changed to `Sign`. -``` -(pm *PmCrypto) SignTextDetached(plaintext string, privateKey *KeyRing, passphrase string, trim bool) (string, error): -* (if just signature) (keyRing *KeyRing) SignDetached(message *PlainMessage) (*PGPSignature, error) -* (if PGP SIGNED MESSAGE) (helper) SignCleartextMessage(keyRing *crypto.KeyRing, text string) (string, error) -* (if PGP SIGNED MESSAGE) (helper) SignCleartextMessageArmored(privateKey, passphrase, text string) (string, error) -``` - -### SignBinDetached -Moved to `KeyRing`. -``` -(pm *PmCrypto) SignBinDetached(plainData []byte, privateKey *KeyRing, passphrase string) (string, error): -* (keyRing *KeyRing) SignDetached(message *PlainMessage) (*PGPSignature, error) -``` - -### VerifyTextSignDetachedBinKey, VerifyBinSignDetachedBinKey -Moved to `KeyRing`, changed to Verify. -See signature_test.go for use examples. -``` -(pm *PmCrypto) VerifyTextSignDetachedBinKey(signature string, plaintext string, publicKey *KeyRing, verifyTime int64) (bool, error): -(pm *PmCrypto) VerifyBinSignDetachedBinKey(signature string, plainData []byte, publicKey *KeyRing, verifyTime int64) (bool, error): -* (to verify) (keyRing *KeyRing) VerifyDetached(message *PlainMessage, signature *PGPSignature, verifyTime int64) (error) -* (if PGP SIGNED MESSAGE) (helper) VerifyCleartextMessage(keyRing *crypto.KeyRing, armored string, verifyTime int64) (string, error) -* (if PGP SIGNED MESSAGE) (helper) VerifyCleartextMessageArmored(publicKey, armored string, verifyTime int64) (string, error) -``` - -## signature_collector.go -No change. - -## time.go -### UpdateTime -No change. - -### GetTimeUnix -Renamed. -``` -(pm *PmCrypto) GetTimeUnix() int64: -(pm *PmCrypto) GetUnixTime() int64 -``` - -### GetTime -No change. diff --git a/README.md b/README.md index 9fa6aaa..fa44aad 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# GopenPGP +# GopenPGP V2 GopenPGP is a high-level OpenPGP library built on top of [a fork of the golang crypto library](https://github.com/ProtonMail/crypto). @@ -10,7 +10,6 @@ crypto library](https://github.com/ProtonMail/crypto). - [Download/Install](#downloadinstall) - [Documentation](#documentation) - [Using with Go Mobile](#using-with-go-mobile) -- [Other notes](#other-notes) - [Full documentation](#full-documentation) - [Examples](#examples) - [Set up](#set-up) @@ -24,102 +23,89 @@ crypto library](https://github.com/ProtonMail/crypto). ## 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.0 +} -This package uses [Go Modules](https://github.com/golang/go/wiki/Modules), and -thus requires Go 1.11+. If you're also using Go Modules, simply import it and -start using it (see [Set up](#set-up)). If not, run: - -```bash -go get github.com/ProtonMail/gopenpgp # or git clone this repository into the following path -cd $GOPATH/src/github.com/ProtonMail/gopenpgp -GO111MODULE=on go mod vendor +replace golang.org/x/crypto => github.com/ProtonMail/crypto v0.0.0-20191122234321-e77a1f03baa0 ``` -(After that, the code will also work in Go 1.10, but you need Go 1.11 for the `go mod` command.) +It can then be installed by running: +```sh +go mod vendor +``` +Finally your software can include it in your software as follows: +```go +package main + +import ( + "fmt" + "github.com/ProtonMail/gopenpgp/v2/crypto" +) + +func main() { + fmt.Println(crypto.GetUnixTime()) +} +``` + +### 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 -https://godoc.org/github.com/ProtonMail/gopenpgp/crypto +In this document examples are provided and the proper use of (almost) all functions is tested. ## Using with Go Mobile - -Setup Go Mobile and build/bind the source code: - -Go Mobile repo: https://github.com/golang/mobile -Go Mobile wiki: https://github.com/golang/go/wiki/Mobile - -1. Install Go: `brew install go` -2. Install Gomobile: `go get -u golang.org/x/mobile/cmd/gomobile` -3. Install Gobind: `go install golang.org/x/mobile/cmd/gobind` -4. Install Android SDK and NDK using Android Studio -5. Set env: `export ANDROID_HOME="/AndroidSDK"` (path to your SDK) -6. Init gomobile: `gomobile init -ndk /AndroidSDK/ndk-bundle/` (path to your NDK) -7. Copy Go module dependencies to the vendor directory: `go mod vendor` -8. Build examples: - `gomobile build -target=android #or ios` - - Bind examples: - `gomobile bind -target ios -o frameworks/name.framework` - `gomobile bind -target android` - - The bind will create framework for iOS and jar&aar files for Android (x86_64 and ARM). - -## Other notes - -If you wish to use build.sh, you may need to modify the paths in it. - -Interfacing between Go and Swift: -https://medium.com/@matryer/tutorial-calling-go-code-from-swift-on-ios-and-vice-versa-with-gomobile-7925620c17a4. - -## Full documentation -The full documentation for this API is available here: https://godoc.org/gopkg.in/ProtonMail/gopenpgp.v0/crypto +The use with gomobile is still to be documented ## Examples -### Set up - -```go -import "github.com/ProtonMail/gopenpgp/crypto" -``` - ### Encrypt / Decrypt with password ```go -import "github.com/ProtonMail/gopenpgp/helper" +import "github.com/ProtonMail/gopenpgp/v2/helper" -const password = "my secret password" +const password = []byte("hunter2") // Encrypt data with password -armor, err := helper.EncryptMessageWithToken(password, "my message") +armor, err := helper.EncryptMessageWithPassword(password, "my message") // Decrypt data with password -message, err := helper.DecryptMessageWithToken(password, armor) +message, err := helper.DecryptMessageWithPassword(password, armor) ``` -To use more encryption algorithms: +To encrypt binary data or use more advanced modes: ```go -import "github.com/ProtonMail/gopenpgp/constants" -import "github.com/ProtonMail/gopenpgp/helper" +import "github.com/ProtonMail/gopenpgp/v2/constants" -// Encrypt data with password -armor, err := helper.EncryptMessageWithTokenAlgo(password, "my message", constants.ThreeDES) +const password = []byte("hunter2") -// Decrypt data with password -message, err := helper.DecryptMessageWithToken(password, armor) -``` - -To encrypt binary data, reuse the key multiple times, or use more advanced modes: -```go -import "github.com/ProtonMail/gopenpgp/constants" - -var key = crypto.NewSymmetricKeyFromToken("my secret password", constants.AES256) var message = crypto.NewPlainMessage(data) +// Or +message = crypto.NewPlainMessageFromString(string) // Encrypt data with password -encrypted, err := key.Encrypt(message) +encrypted, err := EncryptMessageWithPassword(message, password) +// Encrypted message in encrypted.GetBinary() or encrypted.GetArmored() // Decrypt data with password -decrypted, err := key.Decrypt(password, encrypted) +decrypted, err := DecryptMessageWithPassword(encrypted, password) //Original message in decrypted.GetBinary() ``` @@ -127,7 +113,7 @@ decrypted, err := key.Decrypt(password, encrypted) ### Encrypt / Decrypt with PGP keys ```go -import "github.com/ProtonMail/gopenpgp/helper" +import "github.com/ProtonMail/gopenpgp/v2/helper" // put keys in backtick (``) to avoid errors caused by spaces or tabs const pubkey = `-----BEGIN PGP PUBLIC KEY BLOCK----- @@ -138,7 +124,7 @@ const privkey = `-----BEGIN PGP PRIVATE KEY BLOCK----- ... -----END PGP PRIVATE KEY BLOCK-----` // encrypted private key -const passphrase = `the passphrase of the private key` // what the privKey is encrypted with +const passphrase = []byte(`the passphrase of the private key`) // Passphrase of the privKey // encrypt message using public key armor, err := helper.EncryptMessageArmored(pubkey, "plain text") @@ -162,40 +148,51 @@ decrypted, err := helper.DecryptVerifyMessageArmored(pubkey, privkey, passphrase With binary data or advanced modes: ```go // Keys initialization as before (omitted) -var binMessage = NewPlainMessage(data) +var binMessage = crypto.NewPlainMessage(data) + +publicKeyObj, err := crypto.NewKeyFromArmored(publicKey) +publicKeyRing, err := crypto.NewKeyFromArmored(publicKeyObj) -publicKeyRing, err := crypto.BuildKeyRingArmored(publicKey) -privateKeyRing, err := crypto.BuildKeyRingArmored(privateKey) -err = privateKeyRing.UnlockWithPassphrase(passphrase) pgpMessage, err := publicKeyRing.Encrypt(binMessage, privateKeyRing) // Armored message in pgpMessage.GetArmored() // pgpMessage can be obtained from NewPGPMessageFromArmored(ciphertext) +privateKeyObj, err := crypto.NewKeyFromArmored(privateKey) +unlockedKeyObj = privateKeyObj.Unlock(passphrase) +privateKeyRing, err := crypto.NewKeyRing(unlockedKeyObj) + message, err := privateKeyRing.Decrypt(pgpMessage, publicKeyRing, crypto.GetUnixTime()) +privateKeyRing.ClearPrivateParams() + // Original data in message.GetString() // `err` can be a SignatureVerificationError ``` -### Generate key +### 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. ```go const ( - localPart = "name.surname" - domain = "example.com" - passphrase = "LongSecret" + name = "Max Mustermann" + email = "max.mustermann@example.com" + passphrase = []byte("LongSecret") rsaBits = 2048 - ecBits = 256 ) -// RSA -rsaKey, err := crypto.GenerateKey(localPart, domain, passphrase, "rsa", rsaBits) +// RSA, string +rsaKey, err := helper.GenerateKey(name, email, passphrase, "rsa", rsaBits) -// Curve25519 -ecKey, err := crypto.GenerateKey(localPart, domain, passphrase, "x25519", ecBits) +// Curve25519, string +ecKey, err := helper.GenerateKey(name, email, passphrase, "x25519", 0) + +// RSA, Key struct +rsaKey, err := crypto.GenerateKey(name, email, "rsa", rsaBits) + +// Curve25519, Key struct +ecKey, err := crypto.GenerateKey(name, email, "x25519", 0) ``` ### Detached signatures for plain text messages @@ -206,14 +203,14 @@ The output is an armored signature. ```go const privkey = `-----BEGIN PGP PRIVATE KEY BLOCK----- ... ------END PGP PRIVATE KEY BLOCK-----` // encrypted private key -const passphrase = "LongSecret" -const trimNewlines = false +-----END PGP PRIVATE KEY BLOCK-----` // Encrypted private key +const passphrase = []byte("LongSecret") // Private key passphrase -var message = NewPlaintextMessage("Verified message") +var message = crypto.NewPlaintextMessage("Verified message") -signingKeyRing, err := crypto.BuildKeyRingArmored(privkey) -signingKeyRing.UnlockWithPassphrase(passphrase) // if private key is locked with passphrase +privateKeyObj, err := crypto.NewKeyFromArmored(privkey) +unlockedKeyObj = privateKeyObj.Unlock(passphrase) +signingKeyRing, err := crypto.NewKeyRing(unlockedKeyObj) pgpSignature, err := signingKeyRing.SignDetached(message, trimNewlines) @@ -232,9 +229,11 @@ const signature = `-----BEGIN PGP SIGNATURE----- ... -----END PGP SIGNATURE-----` -message := NewPlaintextMessage("Verified message") -pgpSignature, err := NewPGPSignatureFromArmored(signature) -signingKeyRing, err := crypto.BuildKeyRingArmored(pubkey) +message := crypto.NewPlaintextMessage("Verified message") +pgpSignature, err := crypto.NewPGPSignatureFromArmored(signature) + +publicKeyObj, err := crypto.NewKeyFromArmored(pubkey) +signingKeyRing, err := crypto.NewKeyFromArmored(publicKeyObj) err := signingKeyRing.VerifyDetached(message, pgpSignature, crypto.GetUnixTime()) @@ -251,10 +250,11 @@ const privkey = `-----BEGIN PGP PRIVATE KEY BLOCK----- -----END PGP PRIVATE KEY BLOCK-----` // encrypted private key const passphrase = "LongSecret" -var message = NewPlainMessage(data) +var message = crypto.NewPlainMessage(data) -signingKeyRing, err := crypto.BuildKeyRingArmored(privkey) -signingKeyRing.UnlockWithPassphrase(passphrase) // if private key is locked with passphrase +privateKeyObj, err := crypto.NewKeyFromArmored(privkey) +unlockedKeyObj := privateKeyObj.Unlock(passphrase) +signingKeyRing, err := crypto.NewKeyRing(unlockedKeyObj) pgpSignature, err := signingKeyRing.SignDetached(message) @@ -273,9 +273,11 @@ const signature = `-----BEGIN PGP SIGNATURE----- ... -----END PGP SIGNATURE-----` -message := NewPlainMessage("Verified message") -pgpSignature, err := NewPGPSignatureFromArmored(signature) -signingKeyRing, err := crypto.BuildKeyRingArmored(pubkey) +message := crypto.NewPlainMessage("Verified message") +pgpSignature, err := crypto.NewPGPSignatureFromArmored(signature) + +publicKeyObj, err := crypto.NewKeyFromArmored(pubkey) +signingKeyRing, err := crypto.NewKeyFromArmored(publicKeyObj) err := signingKeyRing.VerifyDetached(message, pgpSignature, crypto.GetUnixTime()) @@ -287,35 +289,56 @@ if err == nil { ### Cleartext signed messages ```go // Keys initialization as before (omitted) - -armored, err := SignCleartextMessageArmored(privateKey, passphrase, plaintext) +armored, err := helper.SignCleartextMessageArmored(privateKey, passphrase, plaintext) ``` 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) - -var verifyTime = crypto.GetUnixTime() - -verifiedPlainText, err := VerifyCleartextMessageArmored(publicKey, armored, verifyTime) +verifiedPlainText, err := helper.VerifyCleartextMessageArmored(publicKey, armored, crypto.GetUnixTime()) ``` ### Encrypting and decrypting session Keys +A session key can be generated, encrypted to a Asymmetric/Symmetric key packet and obtained from it ```go // Keys initialization as before (omitted) -symmetricKey := &SymmetricKey{ - Key: "RandomTokenabcdef", - Algo: constants.AES256, -} +sessionKey, err := crypto.GenerateSessionKey() -keyPacket, err := publicKey.EncryptSessionKey(symmetricKey) +keyPacket, err := publicKey.EncryptSessionKey(sessionKey) +keyPacketSymm, err := crypto.EncryptSessionKeyWithPassword(sessionKey, password) ``` -`KeyPacket` is a `[]byte` containing the session key encrypted with the private key. - +`KeyPacket` is a `[]byte` containing the session key encrypted with the private key or password. ```go -outputSymmetricKey, err := privateKey.DecryptSessionKey(keyPacket) +decodedKeyPacket, err := privateKey.DecryptSessionKey(keyPacket) +decodedSymmKeyPacket, err := crypto.DecryptSessionKeyWithPassword(keyPacketSymm, password) ``` -`outputSymmetricKey` is an object of type `*SymmetricKey` that can be used to decrypt the correspondig message. +`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 password +dataPacket, err := sessionKey.Encrypt(message) + +// Decrypt data with password +decrypted, err := sessionKey.Decrypt(password, dataPacket) + +//Original message in decrypted.GetBinary() +``` + +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.GetKeyPacket() +// Data Packet is in newPGPSplitMessage.GetDataPacket() +``` \ No newline at end of file diff --git a/armor/armor.go b/armor/armor.go index 574af0f..9641470 100644 --- a/armor/armor.go +++ b/armor/armor.go @@ -7,8 +7,8 @@ import ( "io" "io/ioutil" - "github.com/ProtonMail/gopenpgp/constants" - "github.com/ProtonMail/gopenpgp/internal" + "github.com/ProtonMail/gopenpgp/v2/constants" + "github.com/ProtonMail/gopenpgp/v2/internal" "golang.org/x/crypto/openpgp/armor" ) @@ -33,11 +33,12 @@ func ArmorWithType(input []byte, armorType string) (string, error) { if err != nil { return "", err } - _, err = w.Write(input) - if err != nil { + if _, err = w.Write(input); err != nil { + return "", err + } + if err := w.Close(); err != nil { return "", err } - w.Close() return b.String(), nil } diff --git a/build.sh b/build.sh deleted file mode 100755 index 2645908..0000000 --- a/build.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash - -SCRIPT_LOCATION=$(cd $(dirname $0); echo $PWD) - -OUTPUT_PATH="dist" -ANDROID_OUT=${OUTPUT_PATH}/"Android" -IOS_OUT=${OUTPUT_PATH}/"iOS" -mkdir -p $ANDROID_OUT -mkdir -p $IOS_OUT - -printf "\e[0;32mStart Building iOS framework .. Location: ${IOS_OUT} \033[0m\n\n" -PACKAGE_PATH=github.com/ProtonMail/gopenpgp - -gomobile bind -target ios -o ${IOS_OUT}/Crypto.framework $PACKAGE_PATH/crypto $PACKAGE_PATH/armor $PACKAGE_PATH/constants $PACKAGE_PATH/models $PACKAGE_PATH/subtle - -printf "\e[0;32mStart Building Android lib .. Location: ${ANDROID_OUT} \033[0m\n\n" - -gomobile bind -target android -javapkg com.proton.gopenpgp -o ${ANDROID_OUT}/gopenpgp.aar $PACKAGE_PATH/crypto $PACKAGE_PATH/armor $PACKAGE_PATH/constants $PACKAGE_PATH/models $PACKAGE_PATH/subtle - -printf "\e[0;32mInstalling frameworks. \033[0m\n\n" - -printf "\e[0;32mAll Done. \033[0m\n\n" - - diff --git a/crypto/attachment.go b/crypto/attachment.go index cdf232c..141b765 100644 --- a/crypto/attachment.go +++ b/crypto/attachment.go @@ -34,8 +34,14 @@ func (ap *AttachmentProcessor) Finish() (*PGPSplitMessage, error) { if ap.err != nil { return nil, ap.err } - (*ap.w).Close() - (*ap.pipe).Close() + if err := (*ap.w).Close(); err != nil { + return nil, err + } + + if err := (*ap.pipe).Close(); err != nil { + return nil, err + } + ap.done.Wait() if ap.garbageCollector > 0 { runtime.GC() diff --git a/crypto/attachment_test.go b/crypto/attachment_test.go index 104622e..dd5d279 100644 --- a/crypto/attachment_test.go +++ b/crypto/attachment_test.go @@ -10,44 +10,46 @@ import ( // 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) } - symmetricKey, err := testPrivateKeyRing.DecryptSessionKey(testKeyPacketsDecoded) + sessionKey, err := keyRingTestPrivate.DecryptSessionKey(testKeyPacketsDecoded) if err != nil { t.Fatal("Expected no error while decrypting KeyPacket, got:", err) } - assert.Exactly(t, testSymmetricKey, symmetricKey) + assert.Exactly(t, testAttachmentKey, sessionKey.Key) } func TestAttachmentSetKey(t *testing.T) { - keyPackets, err := testPublicKeyRing.EncryptSessionKey(testSymmetricKey) + keyPackets, err := keyRingTestPublic.EncryptSessionKey(testSessionKey) if err != nil { t.Fatal("Expected no error while encrypting attachment key, got:", err) } - symmetricKey, err := testPrivateKeyRing.DecryptSessionKey(keyPackets) + sessionKey, err := keyRingTestPrivate.DecryptSessionKey(keyPackets) if err != nil { t.Fatal("Expected no error while decrypting attachment key, got:", err) } - assert.Exactly(t, testSymmetricKey, symmetricKey) + assert.Exactly(t, testSessionKey, sessionKey) } func TestAttachmentEncryptDecrypt(t *testing.T) { var testAttachmentCleartext = "cc,\ndille." var message = NewPlainMessage([]byte(testAttachmentCleartext)) - encSplit, err := testPrivateKeyRing.EncryptAttachment(message, "s.txt") + encSplit, err := keyRingTestPrivate.EncryptAttachment(message, "s.txt") if err != nil { t.Fatal("Expected no error while encrypting attachment, got:", err) } - redecData, err := testPrivateKeyRing.DecryptAttachment(encSplit) + redecData, err := keyRingTestPrivate.DecryptAttachment(encSplit) if err != nil { t.Fatal("Expected no error while decrypting attachment, got:", err) } @@ -59,14 +61,14 @@ func TestAttachmentEncrypt(t *testing.T) { var testAttachmentCleartext = "cc,\ndille." var message = NewPlainMessage([]byte(testAttachmentCleartext)) - encSplit, err := testPrivateKeyRing.EncryptAttachment(message, "s.txt") + encSplit, err := keyRingTestPrivate.EncryptAttachment(message, "s.txt") if err != nil { t.Fatal("Expected no error while encrypting attachment, got:", err) } pgpMessage := NewPGPMessage(encSplit.GetBinary()) - redecData, err := testPrivateKeyRing.Decrypt(pgpMessage, nil, 0) + redecData, err := keyRingTestPrivate.Decrypt(pgpMessage, nil, 0) if err != nil { t.Fatal("Expected no error while decrypting attachment, got:", err) } @@ -78,7 +80,7 @@ func TestAttachmentDecrypt(t *testing.T) { var testAttachmentCleartext = "cc,\ndille." var message = NewPlainMessage([]byte(testAttachmentCleartext)) - encrypted, err := testPrivateKeyRing.Encrypt(message, nil) + encrypted, err := keyRingTestPrivate.Encrypt(message, nil) if err != nil { t.Fatal("Expected no error while encrypting attachment, got:", err) } @@ -93,7 +95,7 @@ func TestAttachmentDecrypt(t *testing.T) { t.Fatal("Expected no error while unarmoring, got:", err) } - redecData, err := testPrivateKeyRing.DecryptAttachment(pgpSplitMessage) + redecData, err := keyRingTestPrivate.DecryptAttachment(pgpSplitMessage) if err != nil { t.Fatal("Expected no error while decrypting attachment, got:", err) } diff --git a/crypto/base_test.go b/crypto/base_test.go index 8b0601d..4df7581 100644 --- a/crypto/base_test.go +++ b/crypto/base_test.go @@ -2,13 +2,21 @@ package crypto import ( "io/ioutil" + "math/big" "strings" + "testing" + + "golang.org/x/crypto/ed25519" + "golang.org/x/crypto/openpgp/ecdh" + "golang.org/x/crypto/rsa" + + "github.com/stretchr/testify/assert" ) -var err error +const testTime = 1557754627 // 2019-05-13T13:37:07+00:00 func readTestFile(name string, trimNewlines bool) string { - data, err := ioutil.ReadFile("testdata/" + name) + data, err := ioutil.ReadFile("testdata/" + name) //nolint if err != nil { panic(err) } @@ -17,3 +25,48 @@ func readTestFile(name string, trimNewlines bool) string { } return string(data) } + +func init() { + UpdateTime(testTime) // 2019-05-13T13:37:07+00:00 + + initGenerateKeys() + initArmoredKeys() + initKeyRings() +} + +func assertBigIntCleared(t *testing.T, x *big.Int) { + w := x.Bits() + for k := range w { + assert.Exactly(t, big.Word(0x00), w[k]) + } +} + +func assertMemCleared(t *testing.T, b []byte) { + for k := range b { + assert.Exactly(t, uint8(0x00), b[k]) + } +} + +func assertRSACleared(t *testing.T, rsaPriv *rsa.PrivateKey) { + assertBigIntCleared(t, rsaPriv.D) + for idx := range rsaPriv.Primes { + assertBigIntCleared(t, rsaPriv.Primes[idx]) + } + assertBigIntCleared(t, rsaPriv.Precomputed.Qinv) + assertBigIntCleared(t, rsaPriv.Precomputed.Dp) + assertBigIntCleared(t, rsaPriv.Precomputed.Dq) + + for idx := range rsaPriv.Precomputed.CRTValues { + assertBigIntCleared(t, rsaPriv.Precomputed.CRTValues[idx].Exp) + assertBigIntCleared(t, rsaPriv.Precomputed.CRTValues[idx].Coeff) + assertBigIntCleared(t, rsaPriv.Precomputed.CRTValues[idx].R) + } +} + +func assertEdDSACleared(t *testing.T, priv ed25519.PrivateKey) { + assertMemCleared(t, priv) +} + +func assertECDHCleared(t *testing.T, priv *ecdh.PrivateKey) { + assertMemCleared(t, priv.D) +} diff --git a/crypto/key.go b/crypto/key.go index 10b8309..c3e9f5a 100644 --- a/crypto/key.go +++ b/crypto/key.go @@ -4,54 +4,353 @@ import ( "bytes" "crypto" "encoding/hex" - "errors" "fmt" + "io" "math/big" + "strconv" "strings" - "github.com/ProtonMail/gopenpgp/armor" - "github.com/ProtonMail/gopenpgp/constants" + "github.com/ProtonMail/gopenpgp/v2/armor" + "github.com/ProtonMail/gopenpgp/v2/constants" + "github.com/pkg/errors" "golang.org/x/crypto/openpgp" + xarmor "golang.org/x/crypto/openpgp/armor" "golang.org/x/crypto/openpgp/packet" ) -// IsKeyExpired checks whether the given (unarmored, binary) key is expired. -func IsKeyExpired(publicKey []byte) (bool, error) { - now := getNow() - pubKeyReader := bytes.NewReader(publicKey) - pubKeyEntries, err := openpgp.ReadKeyRing(pubKeyReader) +// Key contains a single private or public key +type Key struct { + // PGP entities in this keyring. + entity *openpgp.Entity +} + +// --- Create Key object + +// 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 true, err + return nil, err } - for _, e := range pubKeyEntries { - if _, ok := e.EncryptionKey(now); ok { + + return key, nil +} + +// NewKeyFromReader reads an binary data into Key +func NewKeyFromReader(r io.Reader) (key *Key, err error) { + key = &Key{} + err = key.readFrom(r, false) + if err != nil { + return nil, err + } + + return key, nil +} + +// NewKey creates a new key from the first key in the unarmored binary data +func NewKey(binKeys []byte) (key *Key, err error) { + return NewKeyFromReader(bytes.NewReader(binKeys)) +} + +// NewKeyFromArmored creates a new key from the first key in an armored +func NewKeyFromArmored(armored string) (key *Key, err error) { + return NewKeyFromArmoredReader(strings.NewReader(armored)) +} + +// 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) +} + +// --- Operate on key + +// Copy creates a deep copy of the key. +func (key *Key) Copy() (*Key, error) { + serialized, err := key.Serialize() + if err != nil { + return nil, err + } + + return NewKey(serialized) +} + +// Lock locks a copy of the key. +func (key *Key) Lock(passphrase []byte) (*Key, error) { + unlocked, err := key.IsUnlocked() + if err != nil { + return nil, err + } + + if !unlocked { + return nil, errors.New("gopenpgp: key is not unlocked") + } + + lockedKey, err := key.Copy() + if err != nil { + return nil, err + } + + 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 { + if err := sub.PrivateKey.Encrypt(passphrase); err != nil { + return nil, errors.Wrap(err, "gopenpgp: error in locking sub key") + } + } + } + + locked, err := lockedKey.IsLocked() + if err != nil { + return nil, err + } + if !locked { + return nil, errors.New("gopenpgp: unable to lock key") + } + + return lockedKey, nil +} + +// Unlock unlocks a copy of the key +func (key *Key) Unlock(passphrase []byte) (*Key, error) { + isLocked, err := key.IsLocked() + if err != nil { + return nil, err + } + + if !isLocked { + return nil, errors.New("gopenpgp: key is not locked") + } + + unlockedKey, err := key.Copy() + if err != nil { + return nil, err + } + + 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 { + if err := sub.PrivateKey.Decrypt(passphrase); err != nil { + return nil, errors.Wrap(err, "gopenpgp: error in unlocking sub key") + } + } + } + + isUnlocked, err := unlockedKey.IsUnlocked() + if err != nil { + return nil, err + } + if !isUnlocked { + return nil, errors.New("gopenpgp: unable to unlock key") + } + + return unlockedKey, nil +} + +// --- Export key + +func (key *Key) Serialize() ([]byte, error) { + var buffer bytes.Buffer + var err error + + if key.entity.PrivateKey == nil { + err = key.entity.Serialize(&buffer) + } else { + err = key.entity.SerializePrivateNoSign(&buffer, nil) + } + + return buffer.Bytes(), err +} + +func (key *Key) Armor() (string, error) { + serialized, err := key.Serialize() + if err != nil { + return "", err + } + + return armor.ArmorWithType(serialized, constants.PrivateKeyHeader) +} + +// GetArmoredPublicKey returns the armored public keys from this keyring. +func (key *Key) GetArmoredPublicKey() (s string, err error) { + var outBuf bytes.Buffer + aw, err := xarmor.Encode(&outBuf, openpgp.PublicKeyType, nil) + if err != nil { + return "", err + } + + if err = key.entity.Serialize(aw); err != nil { + _ = aw.Close() + return "", err + } + + err = aw.Close() + return outBuf.String(), err +} + +// GetPublicKey returns the unarmored public keys from this keyring. +func (key *Key) GetPublicKey() (b []byte, err error) { + var outBuf bytes.Buffer + if err = key.entity.Serialize(&outBuf); err != nil { + return nil, err + } + + return outBuf.Bytes(), nil +} + +// --- Key object properties + +// IsExpired checks whether the key is expired. +func (key *Key) IsExpired() bool { + _, ok := key.entity.EncryptionKey(getNow()) + return !ok +} + +// IsPrivate returns true if the key is private +func (key *Key) IsPrivate() bool { + return key.entity.PrivateKey != nil +} + +// 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") + } + + for _, sub := range key.entity.Subkeys { + if sub.PrivateKey != nil && !sub.PrivateKey.Encrypted { return false, nil } } - return true, errors.New("keys expired") + + return key.entity.PrivateKey.Encrypted, nil } -// IsArmoredKeyExpired checks whether the given armored key is expired. -func IsArmoredKeyExpired(publicKey string) (bool, error) { - rawPubKey, err := armor.Unarmor(publicKey) - if err != nil { - return false, err +// IsUnlocked checks if a private key is unlocked +func (key *Key) IsUnlocked() (bool, error) { + if key.entity.PrivateKey == nil { + return true, errors.New("gopenpgp: a public key cannot be unlocked") } - return IsKeyExpired(rawPubKey) + + for _, sub := range key.entity.Subkeys { + if sub.PrivateKey != nil && sub.PrivateKey.Encrypted { + return false, nil + } + } + + return !key.entity.PrivateKey.Encrypted, nil +} + +// Check verifies if the public keys match the private key parameters by signing and verifying +func (key *Key) Check() (bool, error) { + var err error + testSign := bytes.Repeat([]byte{0x01}, 64) + testReader := bytes.NewReader(testSign) + + if !key.IsPrivate() { + return false, errors.New("gopenpgp: can check only private key") + } + + var signBuf bytes.Buffer + + if err = openpgp.DetachSign(&signBuf, key.entity, testReader, nil); err != nil { + return false, errors.New("gopenpgp: unable to sign with key") + } + + testReader = bytes.NewReader(testSign) + signer, err := openpgp.CheckDetachedSignature(openpgp.EntityList{key.entity}, testReader, &signBuf, nil) + + if signer == nil || err != nil { + return false, nil + } + + return true, nil +} + +// 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 { + fmt.Println("SubKey:" + hex.EncodeToString(subKey.PublicKey.Fingerprint[:])) + } + } + fmt.Println("PrimaryKey:" + hex.EncodeToString(key.entity.PrimaryKey.Fingerprint[:])) +} + +// GetHexKeyID returns the key ID, hex encoded as a string +func (key *Key) GetHexKeyID() string { + return strconv.FormatUint(key.GetKeyID(), 16) +} + +// GetKeyID returns the key ID, encoded as 8-byte int +func (key *Key) GetKeyID() uint64 { + return key.entity.PrimaryKey.KeyId +} + +// GetFingerprint gets the fingerprint from the key +func (key *Key) GetFingerprint() string { + return hex.EncodeToString(key.entity.PrimaryKey.Fingerprint[:]) +} + +// --- Internal methods + +// readFrom reads unarmored and armored keys from r and adds them to the keyring. +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 { + entities, err = openpgp.ReadKeyRing(r) + } + if err != nil { + return err + } + + if len(entities) > 1 { + return errors.New("gopenpgp: the key contains too many entities") + } + + if len(entities) == 0 { + return errors.New("gopenpgp: the key does not contain any entity") + } + + key.entity = entities[0] + return nil } func generateKey( - name, email, passphrase, keyType string, + name, email string, + keyType string, bits int, prime1, prime2, prime3, prime4 []byte, -) (string, error) { - if len(email) <= 0 { - return "", errors.New("invalid email format") +) (*Key, error) { + if len(email) == 0 { + return nil, errors.New("gopenpgp: invalid email format") } - if len(name) <= 0 { - return "", errors.New("invalid name format") + if len(name) == 0 { + return nil, errors.New("gopenpgp: invalid name format") } comments := "" @@ -84,114 +383,16 @@ func generateKey( newEntity, err := openpgp.NewEntity(name, comments, email, cfg) if err != nil { - return "", err + return nil, err } if err := newEntity.SelfSign(nil); err != nil { - return "", err + return nil, err } - rawPwd := []byte(passphrase) - if newEntity.PrivateKey != nil && !newEntity.PrivateKey.Encrypted { - if err := newEntity.PrivateKey.Encrypt(rawPwd); err != nil { - return "", err - } + if newEntity.PrivateKey == nil { + return nil, errors.New("gopenpgp: error in generating private key") } - for _, sub := range newEntity.Subkeys { - if sub.PrivateKey != nil && !sub.PrivateKey.Encrypted { - if err := sub.PrivateKey.Encrypt(rawPwd); err != nil { - return "", err - } - } - } - - w := bytes.NewBuffer(nil) - if err := newEntity.SerializePrivateNoSign(w, nil); err != nil { - return "", err - } - serialized := w.Bytes() - return armor.ArmorWithType(serialized, constants.PrivateKeyHeader) -} - -// GenerateRSAKeyWithPrimes generates a RSA key using the given primes. -func GenerateRSAKeyWithPrimes( - name, email, passphrase string, - bits int, - primeone, primetwo, primethree, primefour []byte, -) (string, error) { - return generateKey(name, email, passphrase, "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, passphrase, keyType string, bits int) (string, error) { - return generateKey(name, email, passphrase, keyType, bits, nil, nil, nil, nil) -} - -// UpdatePrivateKeyPassphrase decrypts the given armored privateKey with oldPassphrase, -// re-encrypts it with newPassphrase, and returns the new armored key. -func UpdatePrivateKeyPassphrase( - privateKey string, oldPassphrase string, newPassphrase string, -) (string, error) { - privKey := strings.NewReader(privateKey) - privKeyEntries, err := openpgp.ReadArmoredKeyRing(privKey) - if err != nil { - return "", err - } - - oldrawPwd := []byte(oldPassphrase) - newRawPwd := []byte(newPassphrase) - w := bytes.NewBuffer(nil) - for _, e := range privKeyEntries { - if e.PrivateKey != nil && e.PrivateKey.Encrypted { - if err := e.PrivateKey.Decrypt(oldrawPwd); err != nil { - return "", err - } - } - if e.PrivateKey != nil && !e.PrivateKey.Encrypted { - if err := e.PrivateKey.Encrypt(newRawPwd); err != nil { - return "", err - } - } - - for _, sub := range e.Subkeys { - if sub.PrivateKey != nil && sub.PrivateKey.Encrypted { - if err := sub.PrivateKey.Decrypt(oldrawPwd); err != nil { - return "", err - } - } - if sub.PrivateKey != nil && !sub.PrivateKey.Encrypted { - if err := sub.PrivateKey.Encrypt(newRawPwd); err != nil { - return "", err - } - } - } - if err := e.SerializePrivateNoSign(w, nil); err != nil { - return "", err - } - } - - serialized := w.Bytes() - return armor.ArmorWithType(serialized, constants.PrivateKeyHeader) -} - -// PrintFingerprints is a debug helper function that prints the key and subkey fingerprints. -func PrintFingerprints(pubKey string) (string, error) { - pubKeyReader := strings.NewReader(pubKey) - entries, err := openpgp.ReadArmoredKeyRing(pubKeyReader) - if err != nil { - return "", err - } - - for _, e := range entries { - for _, subKey := range e.Subkeys { - if !subKey.Sig.FlagsValid || subKey.Sig.FlagEncryptStorage || subKey.Sig.FlagEncryptCommunications { - fmt.Println("SubKey:" + hex.EncodeToString(subKey.PublicKey.Fingerprint[:])) - } - } - fmt.Println("PrimaryKey:" + hex.EncodeToString(e.PrimaryKey.Fingerprint[:])) - } - return "", nil + return &Key{newEntity}, nil } diff --git a/crypto/key_clear.go b/crypto/key_clear.go new file mode 100644 index 0000000..caf96a8 --- /dev/null +++ b/crypto/key_clear.go @@ -0,0 +1,128 @@ +package crypto + +import ( + "crypto/dsa" + "crypto/ecdsa" + "errors" + "math/big" + + "golang.org/x/crypto/ed25519" + "golang.org/x/crypto/openpgp/ecdh" + "golang.org/x/crypto/openpgp/elgamal" + "golang.org/x/crypto/rsa" +) + +func (sk *SessionKey) Clear() (ok bool) { + clearMem(sk.Key) + return true +} + +func (key *Key) ClearPrivateParams() (ok bool) { + num := key.clearPrivateWithSubkeys() + key.entity.PrivateKey = nil + + for k := range key.entity.Subkeys { + key.entity.Subkeys[k].PrivateKey = nil + } + + return num > 0 +} + +func (key *Key) clearPrivateWithSubkeys() (num int) { + num = 0 + if key.entity.PrivateKey != nil { + err := clearPrivateKey(key.entity.PrivateKey.PrivateKey) + if err == nil { + num++ + } + } + for k := range key.entity.Subkeys { + if key.entity.Subkeys[k].PrivateKey != nil { + err := clearPrivateKey(key.entity.Subkeys[k].PrivateKey.PrivateKey) + if err == nil { + num++ + } + } + } + return num +} + +func clearPrivateKey(privateKey interface{}) error { + switch priv := privateKey.(type) { + case *rsa.PrivateKey: + return clearRSAPrivateKey(priv) + case *dsa.PrivateKey: + return clearDSAPrivateKey(priv) + case *elgamal.PrivateKey: + return clearElGamalPrivateKey(priv) + case *ecdsa.PrivateKey: + return clearECDSAPrivateKey(priv) + case ed25519.PrivateKey: + return clearEdDSAPrivateKey(priv) + case *ecdh.PrivateKey: + return clearECDHPrivateKey(priv) + default: + return errors.New("gopenpgp: unknown private key") + } +} + +func clearBigInt(n *big.Int) { + w := n.Bits() + for k := range w { + w[k] = 0x00 + } +} + +func clearMem(w []byte) { + for k := range w { + w[k] = 0x00 + } +} + +func clearRSAPrivateKey(rsaPriv *rsa.PrivateKey) error { + clearBigInt(rsaPriv.D) + for idx := range rsaPriv.Primes { + clearBigInt(rsaPriv.Primes[idx]) + } + clearBigInt(rsaPriv.Precomputed.Qinv) + clearBigInt(rsaPriv.Precomputed.Dp) + clearBigInt(rsaPriv.Precomputed.Dq) + + for idx := range rsaPriv.Precomputed.CRTValues { + clearBigInt(rsaPriv.Precomputed.CRTValues[idx].Exp) + clearBigInt(rsaPriv.Precomputed.CRTValues[idx].Coeff) + clearBigInt(rsaPriv.Precomputed.CRTValues[idx].R) + } + + return nil +} + +func clearDSAPrivateKey(priv *dsa.PrivateKey) error { + clearBigInt(priv.X) + + return nil +} + +func clearElGamalPrivateKey(priv *elgamal.PrivateKey) error { + clearBigInt(priv.X) + + return nil +} + +func clearECDSAPrivateKey(priv *ecdsa.PrivateKey) error { + clearBigInt(priv.D) + + return nil +} + +func clearEdDSAPrivateKey(priv ed25519.PrivateKey) error { + clearMem(priv) + + return nil +} + +func clearECDHPrivateKey(priv *ecdh.PrivateKey) error { + clearMem(priv.D) + + return nil +} diff --git a/crypto/key_test.go b/crypto/key_test.go index a44ead4..17a0f94 100644 --- a/crypto/key_test.go +++ b/crypto/key_test.go @@ -2,131 +2,209 @@ package crypto import ( "encoding/base64" + "io/ioutil" "regexp" "strings" "testing" - "github.com/stretchr/testify/assert" - + "golang.org/x/crypto/openpgp/armor" "golang.org/x/crypto/rsa" + + "github.com/stretchr/testify/assert" ) -const name = "Richard M. Stallman" -const domain = "rms@protonmail.ch" +const keyTestName = "Max Mustermann" +const keyTestDomain = "max.mustermann@protonmail.ch" -var passphrase = "I love GNU" -var rsaKey, ecKey, rsaPublicKey, ecPublicKey string +var keyTestPassphrase = []byte("I love GNU") var ( - rsaPrivateKeyRing *KeyRing - ecPrivateKeyRing *KeyRing - rsaPublicKeyRing *KeyRing - ecPublicKeyRing *KeyRing + keyTestArmoredRSA string + keyTestArmoredEC string + keyTestRSA *Key + keyTestEC *Key ) -func TestGenerateKeys(t *testing.T) { - rsaKey, err = GenerateKey(name, domain, passphrase, "rsa", 1024) +func initGenerateKeys() { + var err error + keyTestRSA, err = GenerateKey(keyTestName, keyTestDomain, "rsa", 1024) if err != nil { - t.Fatal("Cannot generate RSA key:", err) + panic("Cannot generate RSA key:" + err.Error()) } - ecKey, err = GenerateKey(name, domain, passphrase, "x25519", 256) + keyTestEC, err = GenerateKey(keyTestName, keyTestDomain, "x25519", 256) if err != nil { - t.Fatal("Cannot generate EC key:", err) + panic("Cannot generate EC key:" + err.Error()) + } +} + +func initArmoredKeys() { + var err error + lockedRSA, err := keyTestRSA.Lock(keyTestPassphrase) + if err != nil { + panic("Cannot lock RSA key:" + err.Error()) + } + + keyTestArmoredRSA, err = lockedRSA.Armor() + if err != nil { + panic("Cannot armor protected RSA key:" + err.Error()) + } + + lockedEC, err := keyTestEC.Lock(keyTestPassphrase) + if err != nil { + panic("Cannot lock EC key:" + err.Error()) + } + + keyTestArmoredEC, err = lockedEC.Armor() + if err != nil { + panic("Cannot armor protected EC key:" + err.Error()) + } +} + +func TestArmorKeys(t *testing.T) { + var err error + noPasswordRSA, err := keyTestRSA.Armor() + if err != nil { + t.Fatal("Cannot armor unprotected RSA key:" + err.Error()) + } + + noPasswordEC, err := keyTestEC.Armor() + if err != nil { + t.Fatal("Cannot armor unprotected EC key:" + err.Error()) } rTest := regexp.MustCompile("(?s)^-----BEGIN PGP PRIVATE KEY BLOCK-----.*-----END PGP PRIVATE KEY BLOCK-----$") - assert.Regexp(t, rTest, rsaKey) - assert.Regexp(t, rTest, ecKey) + assert.Regexp(t, rTest, noPasswordRSA) + assert.Regexp(t, rTest, noPasswordEC) + assert.Regexp(t, rTest, keyTestArmoredRSA) + assert.Regexp(t, rTest, keyTestArmoredEC) } -func TestGenerateKeyRings(t *testing.T) { - rsaPrivateKeyRing, err = ReadArmoredKeyRing(strings.NewReader(rsaKey)) +func TestLockUnlockKeys(t *testing.T) { + testLockUnlockKey(t, keyTestArmoredRSA, keyTestPassphrase) + testLockUnlockKey(t, keyTestArmoredEC, keyTestPassphrase) + testLockUnlockKey(t, readTestFile("keyring_privateKey", false), testMailboxPassword) + + publicKey, err := NewKeyFromArmored(readTestFile("keyring_publicKey", false)) if err != nil { - t.Fatal("Cannot read RSA key:", err) + t.Fatal("Cannot unarmor key:", err) } - rsaPublicKey, err = rsaPrivateKeyRing.GetArmoredPublicKey() - if err != nil { - t.Fatal("Cannot extract RSA public key:", err) + _, err = publicKey.IsLocked() + if err == nil { + t.Fatal("Should not be able to check locked on public key:") } - rsaPublicKeyRing, err = ReadArmoredKeyRing(strings.NewReader(rsaPublicKey)) - if err != nil { - t.Fatal("Cannot read RSA public key:", err) + _, err = publicKey.IsUnlocked() + if err == nil { + t.Fatal("Should not be able to check unlocked on public key:") } - err = rsaPrivateKeyRing.UnlockWithPassphrase(passphrase) - if err != nil { - t.Fatal("Cannot decrypt RSA key:", err) + _, err = publicKey.Unlock(testMailboxPassword) + if err == nil { + t.Fatal("Should not be able to unlock public key:") } - ecPrivateKeyRing, err = ReadArmoredKeyRing(strings.NewReader(ecKey)) - if err != nil { - t.Fatal("Cannot read EC key:", err) - } - - ecPublicKey, err = ecPrivateKeyRing.GetArmoredPublicKey() - if err != nil { - t.Fatal("Cannot extract EC public key:", err) - } - - ecPublicKeyRing, err = ReadArmoredKeyRing(strings.NewReader(ecPublicKey)) - if err != nil { - t.Fatal("Cannot read EC public key:", err) - } - - err = ecPrivateKeyRing.UnlockWithPassphrase(passphrase) - if err != nil { - t.Fatal("Cannot decrypt EC key:", err) + _, err = publicKey.Lock(keyTestPassphrase) + if err == nil { + t.Fatal("Should not be able to lock public key:") } } -func TestUpdatePrivateKeysPassphrase(t *testing.T) { - newPassphrase := "I like GNU" - rsaKey, err = UpdatePrivateKeyPassphrase(rsaKey, passphrase, newPassphrase) +func testLockUnlockKey(t *testing.T, armoredKey string, pass []byte) { + var err error + + lockedKey, err := NewKeyFromArmored(armoredKey) if err != nil { - t.Fatal("Error in changing RSA key's passphrase:", err) + t.Fatal("Cannot unarmor key:", err) } - ecKey, err = UpdatePrivateKeyPassphrase(ecKey, passphrase, newPassphrase) + // Check if key is locked + locked, err := lockedKey.IsLocked() if err != nil { - t.Fatal("Error in changing EC key's passphrase:", err) + t.Fatal("Cannot check if key is unlocked:", err) } - passphrase = newPassphrase + if !locked { + t.Fatal("Key should be fully locked") + } + + unlockedKey, err := lockedKey.Unlock(pass) + if err != nil { + t.Fatal("Cannot unlock key:", err) + } + + // Check if key was successfully unlocked + unlocked, err := unlockedKey.IsUnlocked() + if err != nil { + t.Fatal("Cannot check if key is unlocked:", err) + } + + if !unlocked { + t.Fatal("Key should be fully unlocked") + } + + // Check if action is performed on copy + locked, err = lockedKey.IsLocked() + if err != nil { + t.Fatal("Cannot check if key is unlocked:", err) + } + + if !locked { + t.Fatal("Key should be fully locked") + } + + // re-lock key + relockedKey, err := unlockedKey.Lock(keyTestPassphrase) + if err != nil { + t.Fatal("Cannot lock key:", err) + } + + // Check if key was successfully locked + relocked, err := relockedKey.IsLocked() + if err != nil { + t.Fatal("Cannot check if key is unlocked:", err) + } + + if !relocked { + t.Fatal("Key should be fully locked") + } + + // Check if action is performed on copy + unlocked, err = unlockedKey.IsUnlocked() + if err != nil { + t.Fatal("Cannot check if key is unlocked:", err) + } + + if !unlocked { + t.Fatal("Key should be fully unlocked") + } } -func ExamplePrintFingerprints() { - _, _ = PrintFingerprints(readTestFile("keyring_publicKey", false)) +func ExampleKey_PrintFingerprints() { + keyringKey, _ := NewKeyFromArmored(readTestFile("keyring_publicKey", false)) + keyringKey.PrintFingerprints() // Output: // SubKey:37e4bcf09b36e34012d10c0247dc67b5cb8267f6 // PrimaryKey:6e8ba229b0cccaf6962f97953eb6259edf21df24 } -func TestIsArmoredKeyExpired(t *testing.T) { - rsaRes, err := IsArmoredKeyExpired(rsaPublicKey) +func TestIsExpired(t *testing.T) { + assert.Exactly(t, false, keyTestRSA.IsExpired()) + assert.Exactly(t, false, keyTestEC.IsExpired()) + + expiredKey, err := NewKeyFromArmored(readTestFile("key_expiredKey", false)) if err != nil { - t.Fatal("Error in checking expiration of RSA key:", err) + t.Fatal("Cannot unarmor expired key:", err) } - ecRes, err := IsArmoredKeyExpired(ecPublicKey) + futureKey, err := NewKeyFromArmored(readTestFile("key_futureKey", false)) if err != nil { - t.Fatal("Error in checking expiration of EC key:", err) + t.Fatal("Cannot unarmor future key:", err) } - assert.Exactly(t, false, rsaRes) - assert.Exactly(t, false, ecRes) - - UpdateTime(1557754627) // 2019-05-13T13:37:07+00:00 - - expRes, expErr := IsArmoredKeyExpired(readTestFile("key_expiredKey", false)) - futureRes, futureErr := IsArmoredKeyExpired(readTestFile("key_futureKey", false)) - - assert.Exactly(t, true, expRes) - assert.Exactly(t, true, futureRes) - assert.EqualError(t, expErr, "keys expired") - assert.EqualError(t, futureErr, "keys expired") + assert.Exactly(t, true, expiredKey.IsExpired()) + assert.Exactly(t, true, futureKey.IsExpired()) } func TestGenerateKeyWithPrimes(t *testing.T) { @@ -139,24 +217,96 @@ func TestGenerateKeyWithPrimes(t *testing.T) { prime4, _ := base64.StdEncoding.DecodeString( "58UEDXTX29Q9JqvuE3Tn+Qj275CXBnJbA8IVM4d05cPYAZ6H43bPN01pbJqJTJw/cuFxs+8C+HNw3/MGQOExqw==") - staticRsaKey, err := GenerateRSAKeyWithPrimes(name, domain, passphrase, 1024, prime1, prime2, prime3, prime4) + staticRsaKey, err := GenerateRSAKeyWithPrimes(keyTestName, keyTestDomain, 1024, prime1, prime2, prime3, prime4) if err != nil { - t.Fatal("Cannot generate RSA key:", err) - } - rTest := regexp.MustCompile("(?s)^-----BEGIN PGP PRIVATE KEY BLOCK-----.*-----END PGP PRIVATE KEY BLOCK-----$") - assert.Regexp(t, rTest, staticRsaKey) - - staticRsaKeyRing, err := ReadArmoredKeyRing(strings.NewReader(staticRsaKey)) - if err != nil { - t.Fatal("Cannot read RSA key:", err) + t.Fatal("Cannot generate RSA key with primes:", err) } - err = staticRsaKeyRing.UnlockWithPassphrase(passphrase) - if err != nil { - t.Fatal("Cannot decrypt RSA key:", err) - } - - pk := staticRsaKeyRing.GetEntities()[0].PrivateKey.PrivateKey.(*rsa.PrivateKey) - assert.Exactly(t, prime1, pk.Primes[1].Bytes()) - assert.Exactly(t, prime2, pk.Primes[0].Bytes()) + pk := staticRsaKey.entity.PrivateKey.PrivateKey.(*rsa.PrivateKey) + assert.Exactly(t, prime1, pk.Primes[0].Bytes()) + assert.Exactly(t, prime2, pk.Primes[1].Bytes()) +} + +func TestCheckIntegrity(t *testing.T) { + isVerified, err := keyTestRSA.Check() + if err != nil { + t.Fatal("Expected no error while checking correct passphrase, got:", err) + } + + assert.Exactly(t, true, isVerified) +} + +func TestFailCheckIntegrity(t *testing.T) { + // This test is done with ECC because in an RSA key we would need to replace the primes, but maintaining the moduli, + // that is a private struct element. + k1, _ := GenerateKey(keyTestName, keyTestDomain, "x25519", 256) + k2, _ := GenerateKey(keyTestName, keyTestDomain, "x25519", 256) + + k1.entity.PrivateKey.PrivateKey = k2.entity.PrivateKey.PrivateKey // Swap private keys + + k3, err := k1.Copy() + if err != nil { + t.Fatal("Expected no error while locking keyring kr3, got:", err) + } + + isVerified, err := k3.Check() + if err != nil { + t.Fatal("Expected no error while checking correct passphrase, got:", err) + } + + assert.Exactly(t, false, isVerified) +} + +func TestArmorPublicKey(t *testing.T) { + publicKey, err := keyTestRSA.GetPublicKey() + if err != nil { + t.Fatal("Expected no error while obtaining public key, got:", err) + } + + decodedKey, err := NewKey(publicKey) + if err != nil { + t.Fatal("Expected no error while creating public key ring, got:", err) + } + + privateFingerprint := keyTestRSA.GetFingerprint() + publicFingerprint := decodedKey.GetFingerprint() + + assert.Exactly(t, privateFingerprint, publicFingerprint) +} + +func TestGetArmoredPublicKey(t *testing.T) { + privateKey, err := NewKeyFromArmored(readTestFile("keyring_privateKey", false)) + if err != nil { + t.Fatal("Expected no error while unarmouring private key, got:", err) + } + + s, err := privateKey.GetArmoredPublicKey() + if err != nil { + t.Fatal("Expected no error while getting armored public key, got:", err) + } + + // Decode armored keys + block, err := armor.Decode(strings.NewReader(s)) + if err != nil { + t.Fatal("Expected no error while decoding armored public key, got:", err) + } + + expected, err := armor.Decode(strings.NewReader(readTestFile("keyring_publicKey", false))) + if err != nil { + t.Fatal("Expected no error while decoding expected armored public key, got:", err) + } + + assert.Exactly(t, expected.Type, block.Type) + + b, err := ioutil.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) + if err != nil { + t.Fatal("Expected no error while reading expected armored public key body, got:", err) + } + + assert.Exactly(t, eb, b) } diff --git a/crypto/keyring.go b/crypto/keyring.go index 5ab8faf..4bb5c57 100644 --- a/crypto/keyring.go +++ b/crypto/keyring.go @@ -2,19 +2,11 @@ package crypto import ( "bytes" - "crypto/ecdsa" - "crypto/rsa" - "encoding/hex" - "errors" - "io" "time" + "github.com/pkg/errors" "golang.org/x/crypto/openpgp" - "golang.org/x/crypto/openpgp/armor" "golang.org/x/crypto/openpgp/packet" - xrsa "golang.org/x/crypto/rsa" - - armorUtils "github.com/ProtonMail/gopenpgp/armor" ) // KeyRing contains multiple private and public keys. @@ -32,13 +24,52 @@ type Identity struct { Email string } -// GetEntities returns openpgp entities contained in this KeyRing. -func (keyRing *KeyRing) GetEntities() openpgp.EntityList { - return keyRing.entities +// --- New keyrings + +// NewKeyRing creates a new KeyRing, empty if key is nil +func NewKeyRing(key *Key) (*KeyRing, error) { + keyRing := &KeyRing{} + var err error + if key != nil { + err = keyRing.AddKey(key) + } + return keyRing, err } -// GetSigningEntity returns first private unlocked signing entity from keyring. -func (keyRing *KeyRing) GetSigningEntity() (*openpgp.Entity, error) { +// --- Add keys to keyring +func (keyRing *KeyRing) AddKey(key *Key) error { + if key.IsPrivate() { + unlocked, err := key.IsUnlocked() + if err != nil || !unlocked { + return errors.New("gopenpgp: unable to add locked key to a keyring") + } + } + + keyRing.appendKey(key) + return nil +} + +// --- Extract keys from keyring + +// GetKeys returns openpgp keys contained in this KeyRing. +func (keyRing *KeyRing) GetKeys() []*Key { + keys := make([]*Key, keyRing.CountEntities()) + for i, entity := range keyRing.entities { + keys[i] = &Key{entity} + } + return keys +} + +// GetKey returns the n-th openpgp key contained in this KeyRing. +func (keyRing *KeyRing) GetKey(n int) (*Key, error) { + if n >= keyRing.CountEntities() { + return nil, errors.New("gopenpgp: out of bound when fetching key") + } + 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 for _, e := range keyRing.entities { @@ -58,218 +89,20 @@ func (keyRing *KeyRing) GetSigningEntity() (*openpgp.Entity, error) { return signEntity, nil } -// Unlock tries to unlock as many keys as possible with the following password. Note -// that keyrings can contain keys locked with different passwords, and thus -// err == nil does not mean that all keys have been successfully decrypted. -// If err != nil, the password is wrong for every key, and err is the last error -// encountered. -func (keyRing *KeyRing) Unlock(passphrase []byte) error { - // Build a list of keys to decrypt - var keys []*packet.PrivateKey - for _, e := range keyRing.entities { - // Entity.PrivateKey must be a signing key - if e.PrivateKey != nil { - keys = append(keys, e.PrivateKey) - } +// --- Extract info from key - // Entity.Subkeys can be used for encryption - for _, subKey := range e.Subkeys { - if subKey.PrivateKey != nil && (!subKey.Sig.FlagsValid || subKey.Sig.FlagEncryptStorage || - subKey.Sig.FlagEncryptCommunications) { - - keys = append(keys, subKey.PrivateKey) - } - } - } - - if len(keys) == 0 { - return errors.New("gopenpgp: cannot unlock key ring, no private key available") - } - - var err error - var n int - for _, key := range keys { - if !key.Encrypted { - continue // Key already decrypted - } - - if err = key.Decrypt(passphrase); err == nil { - n++ - } - } - - if n == 0 { - return err - } - return nil +// CountEntities returns the number of entities in the keyring +func (keyRing *KeyRing) CountEntities() int { + return len(keyRing.entities) } -// UnlockWithPassphrase is a wrapper for Unlock that uses strings -func (keyRing *KeyRing) UnlockWithPassphrase(passphrase string) error { - return keyRing.Unlock([]byte(passphrase)) -} - -// WriteArmoredPublicKey outputs armored public keys from the keyring to w. -func (keyRing *KeyRing) WriteArmoredPublicKey(w io.Writer) (err error) { - aw, err := armor.Encode(w, openpgp.PublicKeyType, nil) - if err != nil { - return - } - - for _, e := range keyRing.entities { - if err = e.Serialize(aw); err != nil { - aw.Close() - return - } - } - - err = aw.Close() - return -} - -// GetArmoredPublicKey returns the armored public keys from this keyring. -func (keyRing *KeyRing) GetArmoredPublicKey() (s string, err error) { - b := &bytes.Buffer{} - if err = keyRing.WriteArmoredPublicKey(b); err != nil { - return - } - - s = b.String() - return -} - -// WritePublicKey outputs unarmored public keys from the keyring to w. -func (keyRing *KeyRing) WritePublicKey(w io.Writer) (err error) { - for _, e := range keyRing.entities { - if err = e.Serialize(w); err != nil { - return - } - } - - return -} - -// GetPublicKey returns the unarmored public keys from this keyring. -func (keyRing *KeyRing) GetPublicKey() (b []byte, err error) { - var outBuf bytes.Buffer - if err = keyRing.WritePublicKey(&outBuf); err != nil { - return - } - - b = outBuf.Bytes() - return -} - -// GetFingerprint gets the fingerprint from the keyring. -func (keyRing *KeyRing) GetFingerprint() (string, error) { - for _, entity := range keyRing.entities { - fp := entity.PrimaryKey.Fingerprint - return hex.EncodeToString(fp[:]), nil - } - return "", errors.New("can't find public key") -} - -// CheckPassphrase checks if private key passphrase is correct for every sub key. -func (keyRing *KeyRing) CheckPassphrase(passphrase string) bool { - var keys []*packet.PrivateKey - - for _, entity := range keyRing.entities { - keys = append(keys, entity.PrivateKey) - } - var decryptError error - var n int - for _, key := range keys { - if !key.Encrypted { - continue // Key already decrypted - } - if decryptError = key.Decrypt([]byte(passphrase)); decryptError == nil { - n++ - } - } - - return n != 0 -} - -// ReadFrom reads unarmored and armored keys from r and adds them to the keyring. -func (keyRing *KeyRing) ReadFrom(r io.Reader, armored bool) error { - var err error - var entities openpgp.EntityList - if armored { - entities, err = openpgp.ReadArmoredKeyRing(r) - } else { - entities, err = openpgp.ReadKeyRing(r) - } - for _, entity := range entities { - if entity.PrivateKey != nil { - switch entity.PrivateKey.PrivateKey.(type) { - // TODO: type mismatch after crypto lib update, fix this: - case *rsa.PrivateKey: - entity.PrimaryKey = packet.NewRSAPublicKey( - time.Now(), - entity.PrivateKey.PrivateKey.(*rsa.PrivateKey).Public().(*xrsa.PublicKey)) - - case *ecdsa.PrivateKey: - entity.PrimaryKey = packet.NewECDSAPublicKey( - time.Now(), - entity.PrivateKey.PrivateKey.(*ecdsa.PrivateKey).Public().(*ecdsa.PublicKey)) - } - } - for _, subkey := range entity.Subkeys { - if subkey.PrivateKey != nil { - switch subkey.PrivateKey.PrivateKey.(type) { - case *rsa.PrivateKey: - subkey.PublicKey = packet.NewRSAPublicKey( - time.Now(), - subkey.PrivateKey.PrivateKey.(*rsa.PrivateKey).Public().(*xrsa.PublicKey)) - - case *ecdsa.PrivateKey: - subkey.PublicKey = packet.NewECDSAPublicKey( - time.Now(), - subkey.PrivateKey.PrivateKey.(*ecdsa.PrivateKey).Public().(*ecdsa.PublicKey)) - } - } - } - } - if err != nil { - return err - } - - if len(entities) == 0 { - return errors.New("gopenpgp: key ring doesn't contain any key") - } - - keyRing.entities = append(keyRing.entities, entities...) - return nil -} - -// BuildKeyRing reads keyring from binary data -func BuildKeyRing(binKeys []byte) (keyRing *KeyRing, err error) { - keyRing = &KeyRing{} - entriesReader := bytes.NewReader(binKeys) - err = keyRing.ReadFrom(entriesReader, false) - - return -} - -// BuildKeyRingNoError does not return error on fail -func BuildKeyRingNoError(binKeys []byte) (keyRing *KeyRing) { - keyRing, _ = BuildKeyRing(binKeys) - return -} - -// BuildKeyRingArmored reads armored string and returns keyring -func BuildKeyRingArmored(key string) (keyRing *KeyRing, err error) { - keyRaw, err := armorUtils.Unarmor(key) - if err != nil { - return nil, err - } - keyReader := bytes.NewReader(keyRaw) - keyEntries, err := openpgp.ReadKeyRing(keyReader) - return &KeyRing{entities: keyEntries}, err +// CountDecryptionEntities returns the number of entities in the keyring +func (keyRing *KeyRing) CountDecryptionEntities() int { + return len(keyRing.entities.DecryptionKeys()) } // Identities returns the list of identities associated with this key ring. -func (keyRing *KeyRing) Identities() []*Identity { +func (keyRing *KeyRing) GetIdentities() []*Identity { var identities []*Identity for _, e := range keyRing.entities { for _, id := range e.Identities { @@ -282,28 +115,16 @@ func (keyRing *KeyRing) Identities() []*Identity { return identities } -// KeyIds returns array of IDs of keys in this KeyRing. -func (keyRing *KeyRing) KeyIds() []uint64 { - var res []uint64 - for _, e := range keyRing.entities { - res = append(res, e.PrimaryKey.KeyId) +// GetKeyIDs returns array of IDs of keys in this KeyRing. +func (keyRing *KeyRing) GetKeyIDs() []uint64 { + var res = make([]uint64, len(keyRing.entities)) + for id, e := range keyRing.entities { + res[id] = e.PrimaryKey.KeyId } return res } -// ReadArmoredKeyRing reads an armored data into keyring. -func ReadArmoredKeyRing(r io.Reader) (keyRing *KeyRing, err error) { - keyRing = &KeyRing{} - err = keyRing.ReadFrom(r, true) - return -} - -// ReadKeyRing reads an binary data into keyring. -func ReadKeyRing(r io.Reader) (keyRing *KeyRing, err error) { - keyRing = &KeyRing{} - err = keyRing.ReadFrom(r, false) - return -} +// --- Filter keyrings // FilterExpiredKeys takes a given KeyRing list and it returns only those // KeyRings which contain at least, one unexpired Key. It returns only unexpired @@ -316,7 +137,7 @@ func FilterExpiredKeys(contactKeys []*KeyRing) (filteredKeys []*KeyRing, err err for _, contactKeyRing := range contactKeys { keyRingHasUnexpiredEntity := false keyRingHasTotallyExpiredEntity := false - for _, entity := range contactKeyRing.GetEntities() { + for _, entity := range contactKeyRing.entities { hasExpired := false hasUnexpired := false for _, subkey := range entity.Subkeys { @@ -333,7 +154,12 @@ func FilterExpiredKeys(contactKeys []*KeyRing) (filteredKeys []*KeyRing, err err } } if keyRingHasUnexpiredEntity { - filteredKeys = append(filteredKeys, contactKeyRing) + keyRingCopy, err := contactKeyRing.Copy() + if err != nil { + return nil, err + } + + filteredKeys = append(filteredKeys, keyRingCopy) } else if keyRingHasTotallyExpiredEntity { hasExpiredEntity = true } @@ -347,12 +173,57 @@ func FilterExpiredKeys(contactKeys []*KeyRing) (filteredKeys []*KeyRing, err err } // FirstKey returns a KeyRing with only the first key of the original one -func (keyRing *KeyRing) FirstKey() *KeyRing { +func (keyRing *KeyRing) FirstKey() (*KeyRing, error) { if len(keyRing.entities) == 0 { - return nil + return nil, errors.New("gopenpgp: No key available in this keyring") } newKeyRing := &KeyRing{} newKeyRing.entities = keyRing.entities[:1] - return newKeyRing + return newKeyRing.Copy() +} + +// Copy creates a deep copy of the keyring +func (keyRing *KeyRing) Copy() (*KeyRing, error) { + newKeyRing := &KeyRing{} + + entities := make([]*openpgp.Entity, len(keyRing.entities)) + for id, entity := range keyRing.entities { + var buffer bytes.Buffer + var err error + + if entity.PrivateKey == nil { + err = entity.Serialize(&buffer) + } else { + err = entity.SerializePrivateNoSign(&buffer, nil) + } + + if err != nil { + return nil, errors.Wrap(err, "gopenpgp: unable to copy key: error in serializing entity") + } + + bt := buffer.Bytes() + entities[id], err = openpgp.ReadEntity(packet.NewReader(bytes.NewReader(bt))) + + if err != nil { + return nil, errors.Wrap(err, "gopenpgp: unable to copy key: error in reading entity") + } + } + newKeyRing.entities = entities + newKeyRing.FirstKeyID = keyRing.FirstKeyID + + return newKeyRing, nil +} + +func (keyRing *KeyRing) ClearPrivateParams() { + for _, key := range keyRing.GetKeys() { + key.ClearPrivateParams() + } +} + +// INTERNAL FUNCTIONS + +// append appends a key to the keyring +func (keyRing *KeyRing) appendKey(key *Key) { + keyRing.entities = append(keyRing.entities, key.entity) } diff --git a/crypto/keyring_message.go b/crypto/keyring_message.go index 4359d20..ba23d21 100644 --- a/crypto/keyring_message.go +++ b/crypto/keyring_message.go @@ -39,7 +39,7 @@ func (keyRing *KeyRing) Decrypt( // SignDetached generates and returns a PGPSignature for a given PlainMessage func (keyRing *KeyRing) SignDetached(message *PlainMessage) (*PGPSignature, error) { - signEntity, err := keyRing.GetSigningEntity() + signEntity, err := keyRing.getSigningEntity() if err != nil { return nil, err } @@ -56,11 +56,9 @@ func (keyRing *KeyRing) SignDetached(message *PlainMessage) (*PGPSignature, erro // VerifyDetached verifies a PlainMessage with embedded a PGPSignature // and returns a SignatureVerificationError if fails -func (keyRing *KeyRing) VerifyDetached( - message *PlainMessage, signature *PGPSignature, verifyTime int64, -) (error) { +func (keyRing *KeyRing) VerifyDetached(message *PlainMessage, signature *PGPSignature, verifyTime int64) error { return verifySignature( - keyRing.GetEntities(), + keyRing.entities, message.NewReader(), signature.GetBinary(), verifyTime, @@ -78,7 +76,7 @@ func asymmetricEncrypt(data []byte, publicKey *KeyRing, privateKey *KeyRing, isB if privateKey != nil && len(privateKey.entities) > 0 { var err error - signEntity, err = privateKey.GetSigningEntity() + signEntity, err = privateKey.getSigningEntity() if err != nil { return nil, err } @@ -101,8 +99,11 @@ func asymmetricEncrypt(data []byte, publicKey *KeyRing, privateKey *KeyRing, isB } _, err = encryptWriter.Write(data) - encryptWriter.Close() + if err != nil { + return nil, err + } + err = encryptWriter.Close() if err != nil { return nil, err } @@ -114,11 +115,11 @@ func asymmetricEncrypt(data []byte, publicKey *KeyRing, privateKey *KeyRing, isB func asymmetricDecrypt( encryptedIO io.Reader, privateKey *KeyRing, verifyKey *KeyRing, verifyTime int64, ) (plaintext []byte, err error) { - privKeyEntries := privateKey.GetEntities() + privKeyEntries := privateKey.entities var additionalEntries openpgp.EntityList if verifyKey != nil { - additionalEntries = verifyKey.GetEntities() + additionalEntries = verifyKey.entities } if additionalEntries != nil { diff --git a/crypto/session.go b/crypto/keyring_session.go similarity index 56% rename from crypto/session.go rename to crypto/keyring_session.go index 840d98b..6f80df7 100644 --- a/crypto/session.go +++ b/crypto/keyring_session.go @@ -2,31 +2,15 @@ package crypto import ( "bytes" - "errors" "fmt" - "io" + + "github.com/pkg/errors" "golang.org/x/crypto/openpgp/packet" ) -// RandomToken generated a random token of the same size of the keysize of the default cipher. -func RandomToken() ([]byte, error) { - config := &packet.Config{DefaultCipher: packet.CipherAES256} - return RandomTokenSize(config.DefaultCipher.KeySize()) -} - -// RandomTokenSize generates a random token with the specified key size -func RandomTokenSize(size int) ([]byte, error) { - config := &packet.Config{DefaultCipher: packet.CipherAES256} - symKey := make([]byte, size) - if _, err := io.ReadFull(config.Random(), symKey); err != nil { - return nil, err - } - return symKey, nil -} - // DecryptSessionKey returns the decrypted session key from a binary encrypted session key packet. -func (keyRing *KeyRing) DecryptSessionKey(keyPacket []byte) (*SymmetricKey, error) { +func (keyRing *KeyRing) DecryptSessionKey(keyPacket []byte) (*SessionKey, error) { keyReader := bytes.NewReader(keyPacket) packets := packet.NewReader(keyReader) @@ -57,18 +41,21 @@ func (keyRing *KeyRing) DecryptSessionKey(keyPacket []byte) (*SymmetricKey, erro return nil, errors.New("gopenpgp: unable to decrypt session key") } - return newSymmetricKeyFromEncrypted(ek) + 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(sessionSplit *SymmetricKey) ([]byte, error) { +func (keyRing *KeyRing) EncryptSessionKey(sk *SessionKey) ([]byte, error) { outbuf := &bytes.Buffer{} - cf := sessionSplit.GetCipherFunc() + cf, err := sk.GetCipherFunc() + if err != nil { + return nil, errors.Wrap(err, "gopenpgp: unable to encrypt session key") + } var pub *packet.PublicKey - for _, e := range keyRing.GetEntities() { + for _, e := range keyRing.entities { if encryptionKey, ok := e.EncryptionKey(getNow()); ok { pub = encryptionKey.PublicKey break @@ -78,7 +65,7 @@ func (keyRing *KeyRing) EncryptSessionKey(sessionSplit *SymmetricKey) ([]byte, e return nil, errors.New("cannot set key: no public key available") } - if err := packet.SerializeEncryptedKey(outbuf, pub, cf, sessionSplit.Key, nil); err != nil { + if err := packet.SerializeEncryptedKey(outbuf, pub, cf, sk.Key, nil); err != nil { err = fmt.Errorf("gopenpgp: cannot set key: %v", err) return nil, err } diff --git a/crypto/keyring_test.go b/crypto/keyring_test.go index 46a8093..226cd20 100644 --- a/crypto/keyring_test.go +++ b/crypto/keyring_test.go @@ -1,38 +1,27 @@ package crypto import ( - "encoding/base64" - "io/ioutil" - "strings" "testing" - "golang.org/x/crypto/openpgp/armor" - - "github.com/ProtonMail/gopenpgp/constants" "github.com/stretchr/testify/assert" + + "golang.org/x/crypto/ed25519" + "golang.org/x/crypto/openpgp/ecdh" + "golang.org/x/crypto/rsa" ) -var decodedSymmetricKey, _ = base64.StdEncoding.DecodeString("ExXmnSiQ2QCey20YLH6qlLhkY3xnIBC1AwlIXwK/HvY=") - -var testSymmetricKey = &SymmetricKey{ - Key: decodedSymmetricKey, - Algo: constants.AES256, -} - -var testWrongSymmetricKey = &SymmetricKey{ - Key: []byte("WrongPass"), - Algo: constants.AES256, -} +var testSymmetricKey []byte // Corresponding key in testdata/keyring_privateKey -const testMailboxPassword = "apple" +var testMailboxPassword = []byte("apple") // Corresponding key in testdata/keyring_privateKeyLegacy -// const testMailboxPasswordLegacy = "123" +// const testMailboxPasswordLegacy = [][]byte{ []byte("123") } var ( - testPrivateKeyRing *KeyRing - testPublicKeyRing *KeyRing + keyRingTestPrivate *KeyRing + keyRingTestPublic *KeyRing + keyRingTestMultiple *KeyRing ) var testIdentity = &Identity{ @@ -40,75 +29,83 @@ var testIdentity = &Identity{ Email: "", } -func init() { +func initKeyRings() { var err error - testPrivateKeyRing, err = BuildKeyRingArmored(readTestFile("keyring_privateKey", false)) + testSymmetricKey, err = RandomToken(32) if err != nil { - panic(err) + panic("Expected no error while generating random token, got:" + err.Error()) } - testPublicKeyRing, err = BuildKeyRingArmored(readTestFile("keyring_publicKey", false)) + privateKey, err := NewKeyFromArmored(readTestFile("keyring_privateKey", false)) if err != nil { - panic(err) + panic("Expected no error while unarmoring private key, got:" + err.Error()) } - err = testPrivateKeyRing.UnlockWithPassphrase(testMailboxPassword) - if err != nil { - panic(err) - } -} - -func TestKeyRing_ArmoredPublicKeyString(t *testing.T) { - s, err := testPrivateKeyRing.GetArmoredPublicKey() - if err != nil { - t.Fatal("Expected no error while getting armored public key, got:", err) + keyRingTestPrivate, err = NewKeyRing(privateKey) + if err == nil { + panic("Able to create a keyring with a locked key") } - // Decode armored keys - block, err := armor.Decode(strings.NewReader(s)) + unlockedKey, err := privateKey.Unlock(testMailboxPassword) if err != nil { - t.Fatal("Expected no error while decoding armored public key, got:", err) + panic("Expected no error while unlocking private key, got:" + err.Error()) } - expected, err := armor.Decode(strings.NewReader(readTestFile("keyring_publicKey", false))) + keyRingTestPrivate, err = NewKeyRing(unlockedKey) if err != nil { - t.Fatal("Expected no error while decoding expected armored public key, got:", err) + panic("Expected no error while building private keyring, got:" + err.Error()) } - assert.Exactly(t, expected.Type, block.Type) - - b, err := ioutil.ReadAll(block.Body) + publicKey, err := NewKeyFromArmored(readTestFile("keyring_publicKey", false)) if err != nil { - t.Fatal("Expected no error while reading armored public key body, got:", err) + panic("Expected no error while unarmoring public key, got:" + err.Error()) } - eb, err := ioutil.ReadAll(expected.Body) + keyRingTestPublic, err = NewKeyRing(publicKey) if err != nil { - t.Fatal("Expected no error while reading expected armored public key body, got:", err) + panic("Expected no error while building public keyring, got:" + err.Error()) } - assert.Exactly(t, eb, b) -} + keyRingTestMultiple, err = NewKeyRing(nil) + if err != nil { + panic("Expected no error while building empty keyring, got:" + err.Error()) + } -func TestCheckPassphrase(t *testing.T) { - encryptedKeyRing, _ := BuildKeyRingArmored(readTestFile("keyring_privateKey", false)) - isCorrect := encryptedKeyRing.CheckPassphrase("Wrong password") - assert.Exactly(t, false, isCorrect) + err = keyRingTestMultiple.AddKey(keyTestRSA) + if err != nil { + panic("Expected no error while adding RSA key to keyring, got:" + err.Error()) + } - isCorrect = encryptedKeyRing.CheckPassphrase(testMailboxPassword) - assert.Exactly(t, true, isCorrect) + err = keyRingTestMultiple.AddKey(keyTestEC) + if err != nil { + panic("Expected no error while adding EC key to keyring, got:" + err.Error()) + } + + err = keyRingTestMultiple.AddKey(unlockedKey) + if err != nil { + panic("Expected no error while adding unlocked key to keyring, got:" + err.Error()) + } } func TestIdentities(t *testing.T) { - identities := testPrivateKeyRing.Identities() + identities := keyRingTestPrivate.GetIdentities() assert.Len(t, identities, 1) assert.Exactly(t, identities[0], testIdentity) } func TestFilterExpiredKeys(t *testing.T) { - expiredKey, _ := BuildKeyRingArmored(readTestFile("key_expiredKey", false)) - keys := []*KeyRing{testPrivateKeyRing, expiredKey} + expiredKey, err := NewKeyFromArmored(readTestFile("key_expiredKey", false)) + if err != nil { + t.Fatal("Cannot unarmor expired key:", err) + } + + expiredKeyRing, err := NewKeyRing(expiredKey) + if err != nil { + t.Fatal("Cannot create keyring with expired key:", err) + } + + keys := []*KeyRing{keyRingTestPrivate, expiredKeyRing} unexpired, err := FilterExpiredKeys(keys) if err != nil { @@ -116,60 +113,89 @@ func TestFilterExpiredKeys(t *testing.T) { } assert.Len(t, unexpired, 1) - assert.Exactly(t, unexpired[0], testPrivateKeyRing) -} - -func TestGetPublicKey(t *testing.T) { - publicKey, err := testPrivateKeyRing.GetPublicKey() - if err != nil { - t.Fatal("Expected no error while obtaining public key, got:", err) - } - - publicKeyRing, err := BuildKeyRing(publicKey) - if err != nil { - t.Fatal("Expected no error while creating public key ring, got:", err) - } - - privateFingerprint, err := testPrivateKeyRing.GetFingerprint() - if err != nil { - t.Fatal("Expected no error while extracting private fingerprint, got:", err) - } - - publicFingerprint, err := publicKeyRing.GetFingerprint() - if err != nil { - t.Fatal("Expected no error while extracting public fingerprint, got:", err) - } - - assert.Exactly(t, privateFingerprint, publicFingerprint) + assert.Exactly(t, unexpired[0].GetKeyIDs(), keyRingTestPrivate.GetKeyIDs()) } func TestKeyIds(t *testing.T) { - keyIDs := testPrivateKeyRing.KeyIds() + keyIDs := keyRingTestPrivate.GetKeyIDs() var assertKeyIDs = []uint64{4518840640391470884} assert.Exactly(t, assertKeyIDs, keyIDs) } -func TestMutlipleKeyRing(t *testing.T) { - testPublicKeyRing, _ = BuildKeyRingArmored(readTestFile("keyring_publicKey", false)) - assert.Exactly(t, 1, len(testPublicKeyRing.entities)) +func TestMultipleKeyRing(t *testing.T) { + assert.Exactly(t, 3, len(keyRingTestMultiple.entities)) + assert.Exactly(t, 3, keyRingTestMultiple.CountEntities()) + assert.Exactly(t, 3, keyRingTestMultiple.CountDecryptionEntities()) - ids := testPublicKeyRing.KeyIds() - assert.Exactly(t, uint64(0x3eb6259edf21df24), ids[0]) + assert.Exactly(t, 3, len(keyRingTestMultiple.GetKeys())) - err = testPublicKeyRing.ReadFrom(strings.NewReader(readTestFile("mime_publicKey", false)), true) + testKey, err := keyRingTestMultiple.GetKey(1) if err != nil { - t.Fatal("Expected no error while adding a key to the keyring, got:", err) + t.Fatal("Expected no error while extracting key, got:", err) + } + assert.Exactly(t, keyTestEC, testKey) + + _, err = keyRingTestMultiple.GetKey(3) + assert.NotNil(t, err) + + singleKeyRing, err := keyRingTestMultiple.FirstKey() + if err != nil { + t.Fatal("Expected no error while filtering the first key, got:", err) + } + assert.Exactly(t, 1, len(singleKeyRing.entities)) + assert.Exactly(t, 1, singleKeyRing.CountEntities()) + assert.Exactly(t, 1, singleKeyRing.CountDecryptionEntities()) +} + +func TestClearPrivateKey(t *testing.T) { + keyRingCopy, err := keyRingTestMultiple.Copy() + if err != nil { + t.Fatal("Expected no error while copying keyring, got:", err) } - assert.Exactly(t, 2, len(testPublicKeyRing.entities)) + for _, key := range keyRingCopy.GetKeys() { + assert.Nil(t, clearPrivateKey(key.entity.PrivateKey.PrivateKey)) + } - ids = testPublicKeyRing.KeyIds() - assert.Exactly(t, uint64(0x3eb6259edf21df24), ids[0]) - assert.Exactly(t, uint64(0x374130b32ee1e5ea), ids[1]) - - singleKey := testPublicKeyRing.FirstKey() - assert.Exactly(t, 1, len(singleKey.entities)) - - ids = singleKey.KeyIds() - assert.Exactly(t, uint64(0x3eb6259edf21df24), ids[0]) + keys := keyRingCopy.GetKeys() + assertRSACleared(t, keys[0].entity.PrivateKey.PrivateKey.(*rsa.PrivateKey)) + assertEdDSACleared(t, keys[1].entity.PrivateKey.PrivateKey.(ed25519.PrivateKey)) + assertRSACleared(t, keys[2].entity.PrivateKey.PrivateKey.(*rsa.PrivateKey)) +} + +func TestClearPrivateWithSubkeys(t *testing.T) { + keyRingCopy, err := keyRingTestMultiple.Copy() + if err != nil { + t.Fatal("Expected no error while copying keyring, got:", err) + } + + for _, key := range keyRingCopy.GetKeys() { + assert.Exactly(t, 2, key.clearPrivateWithSubkeys()) + } + + keys := keyRingCopy.GetKeys() + assertRSACleared(t, keys[0].entity.PrivateKey.PrivateKey.(*rsa.PrivateKey)) + assertRSACleared(t, keys[0].entity.Subkeys[0].PrivateKey.PrivateKey.(*rsa.PrivateKey)) + + assertEdDSACleared(t, keys[1].entity.PrivateKey.PrivateKey.(ed25519.PrivateKey)) + assertECDHCleared(t, keys[1].entity.Subkeys[0].PrivateKey.PrivateKey.(*ecdh.PrivateKey)) + + assertRSACleared(t, keys[2].entity.PrivateKey.PrivateKey.(*rsa.PrivateKey)) + assertRSACleared(t, keys[2].entity.Subkeys[0].PrivateKey.PrivateKey.(*rsa.PrivateKey)) +} + +func TestClearPrivateParams(t *testing.T) { + keyRingCopy, err := keyRingTestMultiple.Copy() + if err != nil { + t.Fatal("Expected no error while copying keyring, got:", err) + } + + for _, key := range keyRingCopy.GetKeys() { + assert.True(t, key.IsPrivate()) + assert.True(t, key.ClearPrivateParams()) + assert.False(t, key.IsPrivate()) + assert.Nil(t, key.entity.PrivateKey) + assert.Nil(t, key.entity.Subkeys[0].PrivateKey) + assert.False(t, key.ClearPrivateParams()) + } } diff --git a/crypto/message.go b/crypto/message.go index 8f4fe30..d8d8592 100644 --- a/crypto/message.go +++ b/crypto/message.go @@ -10,9 +10,9 @@ import ( "regexp" "runtime" - "github.com/ProtonMail/gopenpgp/armor" - "github.com/ProtonMail/gopenpgp/constants" - "github.com/ProtonMail/gopenpgp/internal" + "github.com/ProtonMail/gopenpgp/v2/armor" + "github.com/ProtonMail/gopenpgp/v2/constants" + "github.com/ProtonMail/gopenpgp/v2/internal" "golang.org/x/crypto/openpgp/clearsign" "golang.org/x/crypto/openpgp/packet" @@ -51,7 +51,7 @@ type PGPSplitMessage struct { // A Cleartext message is a signed PGP message, that is not encrypted, // i.e. the ones beginning with -----BEGIN PGP SIGNED MESSAGE----- type ClearTextMessage struct { - Data []byte + Data []byte Signature []byte } @@ -146,7 +146,7 @@ func NewPGPSignatureFromArmored(armored string) (*PGPSignature, error) { // NewClearTextMessage generates a new ClearTextMessage from data and signature func NewClearTextMessage(data []byte, signature []byte) *ClearTextMessage { return &ClearTextMessage{ - Data: data, + Data: data, Signature: signature, } } @@ -183,7 +183,7 @@ func (msg *PlainMessage) GetBase64() string { return base64.StdEncoding.EncodeToString(msg.Data) } -// NewReader returns a New io.Reader for the bianry data of the message +// NewReader returns a New io.Reader for the binary data of the message func (msg *PlainMessage) NewReader() io.Reader { return bytes.NewReader(msg.GetBinary()) } @@ -203,7 +203,7 @@ func (msg *PGPMessage) GetBinary() []byte { return msg.Data } -// NewReader returns a New io.Reader for the unarmored bianry data of the message +// NewReader returns a New io.Reader for the unarmored binary data of the message func (msg *PGPMessage) NewReader() io.Reader { return bytes.NewReader(msg.GetBinary()) } @@ -225,7 +225,7 @@ func (msg *PGPSplitMessage) GetBinaryKeyPacket() []byte { // GetBinary returns the unarmored binary joined packets as a []byte func (msg *PGPSplitMessage) GetBinary() []byte { - return append(msg.KeyPacket , msg.DataPacket...) + return append(msg.KeyPacket, msg.DataPacket...) } // GetArmored returns the armored message as a string, with joined data and key packets @@ -233,6 +233,11 @@ func (msg *PGPSplitMessage) GetArmored() (string, error) { return armor.ArmorWithType(msg.GetBinary(), constants.PGPMessageHeader) } +// 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...)) +} + // SeparateKeyAndData returns the first keypacket and the (hopefully unique) dataPacket (not verified) // * estimatedLength is the estimate length of the message // * garbageCollector > 0 activates the garbage collector @@ -244,7 +249,6 @@ func (msg *PGPMessage) SeparateKeyAndData(estimatedLength, garbageCollector int) // Store encrypted key and symmetrically encrypted packet separately var encryptedKey *packet.EncryptedKey - var decryptErr error for { var p packet.Packet if p, err = packets.Next(); err == io.EOF { @@ -259,7 +263,7 @@ func (msg *PGPMessage) SeparateKeyAndData(estimatedLength, garbageCollector int) encryptedKey = p case *packet.SymmetricallyEncrypted: - // FIXME: add support for multiple keypackets + // TODO: add support for multiple keypackets var b bytes.Buffer // 2^16 is an estimation of the size difference between input and output, the size difference is most probably // 16 bytes at a maximum though. @@ -267,8 +271,14 @@ func (msg *PGPMessage) SeparateKeyAndData(estimatedLength, garbageCollector int) // in low-memory environments b.Grow(1<<16 + estimatedLength) // empty encoded length + start byte - b.Write(make([]byte, 6)) - b.WriteByte(byte(1)) + if _, err := b.Write(make([]byte, 6)); err != nil { + return nil, err + } + + if err := b.WriteByte(byte(1)); err != nil { + return nil, err + } + actualLength := 1 block := make([]byte, 128) for { @@ -276,7 +286,9 @@ func (msg *PGPMessage) SeparateKeyAndData(estimatedLength, garbageCollector int) if err == io.EOF { break } - b.Write(block[:n]) + if _, err := b.Write(block[:n]); err != nil { + return nil, err + } actualLength += n gcCounter += n if gcCounter > garbageCollector && garbageCollector > 0 { @@ -287,17 +299,18 @@ func (msg *PGPMessage) SeparateKeyAndData(estimatedLength, garbageCollector int) // quick encoding symEncryptedData := b.Bytes() - if actualLength < 192 { + switch { + case actualLength < 192: symEncryptedData[4] = byte(210) symEncryptedData[5] = byte(actualLength) symEncryptedData = symEncryptedData[4:] - } else if actualLength < 8384 { - actualLength = actualLength - 192 + case actualLength < 8384: + actualLength -= 192 symEncryptedData[3] = byte(210) symEncryptedData[4] = 192 + byte(actualLength>>8) symEncryptedData[5] = byte(actualLength) symEncryptedData = symEncryptedData[3:] - } else { + default: symEncryptedData[0] = byte(210) symEncryptedData[1] = byte(255) symEncryptedData[2] = byte(actualLength >> 24) @@ -305,13 +318,9 @@ func (msg *PGPMessage) SeparateKeyAndData(estimatedLength, garbageCollector int) symEncryptedData[4] = byte(actualLength >> 8) symEncryptedData[5] = byte(actualLength) } - outSplit.DataPacket = symEncryptedData } } - if decryptErr != nil { - return nil, fmt.Errorf("gopenpgp: cannot decrypt encrypted key packet: %v", decryptErr) - } if encryptedKey == nil { return nil, errors.New("gopenpgp: packets don't include an encrypted key packet") } diff --git a/crypto/message_test.go b/crypto/message_test.go index 878ba35..5e52355 100644 --- a/crypto/message_test.go +++ b/crypto/message_test.go @@ -4,48 +4,47 @@ import ( "bytes" "encoding/base64" "io" - "strings" "testing" "github.com/stretchr/testify/assert" "golang.org/x/crypto/openpgp/packet" ) -func TestTextMessageEncryptionWithSymmetricKey(t *testing.T) { +func TestTextMessageEncryptionWithPassword(t *testing.T) { var message = NewPlainMessageFromString("The secret code is... 1, 2, 3, 4, 5") // Encrypt data with password - encrypted, err := testSymmetricKey.Encrypt(message) + encrypted, err := EncryptMessageWithPassword(message, testSymmetricKey) if err != nil { t.Fatal("Expected no error when encrypting, got:", err) } // Decrypt data with wrong password - _, err = testWrongSymmetricKey.Decrypt(encrypted) + _, err = DecryptMessageWithPassword(encrypted, []byte("Wrong password")) assert.NotNil(t, err) // Decrypt data with the good password - decrypted, err := testSymmetricKey.Decrypt(encrypted) + decrypted, err := DecryptMessageWithPassword(encrypted, testSymmetricKey) if err != nil { t.Fatal("Expected no error when decrypting, got:", err) } assert.Exactly(t, message.GetString(), decrypted.GetString()) } -func TestBinaryMessageEncryptionWithSymmetricKey(t *testing.T) { +func TestBinaryMessageEncryptionWithPassword(t *testing.T) { binData, _ := base64.StdEncoding.DecodeString("ExXmnSiQ2QCey20YLH6qlLhkY3xnIBC1AwlIXwK/HvY=") var message = NewPlainMessage(binData) // Encrypt data with password - encrypted, err := testSymmetricKey.Encrypt(message) + encrypted, err := EncryptMessageWithPassword(message, testSymmetricKey) if err != nil { t.Fatal("Expected no error when encrypting, got:", err) } // Decrypt data with wrong password - _, err = testWrongSymmetricKey.Decrypt(encrypted) + _, err = DecryptMessageWithPassword(encrypted, []byte("Wrong password")) assert.NotNil(t, err) // Decrypt data with the good password - decrypted, err := testSymmetricKey.Decrypt(encrypted) + decrypted, err := DecryptMessageWithPassword(encrypted, testSymmetricKey) if err != nil { t.Fatal("Expected no error when decrypting, got:", err) } @@ -55,21 +54,12 @@ func TestBinaryMessageEncryptionWithSymmetricKey(t *testing.T) { func TestTextMessageEncryption(t *testing.T) { var message = NewPlainMessageFromString("plain text") - testPublicKeyRing, _ = BuildKeyRingArmored(readTestFile("keyring_publicKey", false)) - testPrivateKeyRing, err = BuildKeyRingArmored(readTestFile("keyring_privateKey", false)) - - // Password defined in keyring_test - err = testPrivateKeyRing.UnlockWithPassphrase(testMailboxPassword) - if err != nil { - t.Fatal("Expected no error unlocking privateKey, got:", err) - } - - ciphertext, err := testPublicKeyRing.Encrypt(message, testPrivateKeyRing) + ciphertext, err := keyRingTestPublic.Encrypt(message, keyRingTestPrivate) if err != nil { t.Fatal("Expected no error when encrypting, got:", err) } - decrypted, err := testPrivateKeyRing.Decrypt(ciphertext, testPublicKeyRing, GetUnixTime()) + decrypted, err := keyRingTestPrivate.Decrypt(ciphertext, keyRingTestPublic, GetUnixTime()) if err != nil { t.Fatal("Expected no error when decrypting, got:", err) } @@ -80,28 +70,19 @@ func TestBinaryMessageEncryption(t *testing.T) { binData, _ := base64.StdEncoding.DecodeString("ExXmnSiQ2QCey20YLH6qlLhkY3xnIBC1AwlIXwK/HvY=") var message = NewPlainMessage(binData) - testPublicKeyRing, _ = BuildKeyRingArmored(readTestFile("keyring_publicKey", false)) - testPrivateKeyRing, err = BuildKeyRingArmored(readTestFile("keyring_privateKey", false)) - - // Password defined in keyring_test - err = testPrivateKeyRing.UnlockWithPassphrase(testMailboxPassword) - if err != nil { - t.Fatal("Expected no error unlocking privateKey, got:", err) - } - - ciphertext, err := testPublicKeyRing.Encrypt(message, testPrivateKeyRing) + ciphertext, err := keyRingTestPublic.Encrypt(message, keyRingTestPrivate) if err != nil { t.Fatal("Expected no error when encrypting, got:", err) } - decrypted, err := testPrivateKeyRing.Decrypt(ciphertext, testPublicKeyRing, GetUnixTime()) + 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 = testPrivateKeyRing.Decrypt(ciphertext, nil, 0) + decrypted, err = keyRingTestPrivate.Decrypt(ciphertext, nil, 0) if err != nil { t.Fatal("Expected no error when decrypting, got:", err) } @@ -109,29 +90,40 @@ func TestBinaryMessageEncryption(t *testing.T) { } func TestIssue11(t *testing.T) { - myKeyring, err := BuildKeyRingArmored(readTestFile("issue11_privatekey", false)) + var issue11Password = []byte("1234") + + issue11Key, err := NewKeyFromArmored(readTestFile("issue11_privatekey", false)) + if err != nil { + t.Fatal("Expected no error while unarmoring private keyring, got:", err) + } + + issue11Key, err = issue11Key.Unlock(issue11Password) + if err != nil { + t.Fatal("Expected no error while unlocking private key, got:", err) + } + + issue11Keyring, err := NewKeyRing(issue11Key) if err != nil { t.Fatal("Expected no error while bulding private keyring, got:", err) } - err = myKeyring.UnlockWithPassphrase("1234"); + senderKey, err := NewKeyFromArmored(readTestFile("issue11_publickey", false)) if err != nil { - t.Fatal("Expected no error while unlocking private keyring, got:", err) + t.Fatal("Expected no error while unarmoring public keyring, got:", err) } + assert.Exactly(t, "643b3595e6ee4fdf", senderKey.GetHexKeyID()) - senderKeyring, err := BuildKeyRingArmored(readTestFile("issue11_publickey", false)) + senderKeyring, err := NewKeyRing(senderKey) if err != nil { t.Fatal("Expected no error while building public keyring, got:", err) } - assert.Exactly(t, []uint64{0x643b3595e6ee4fdf}, senderKeyring.KeyIds()) - pgpMessage, err := NewPGPMessageFromArmored(readTestFile("issue11_message", false)) if err != nil { t.Fatal("Expected no error while unlocking private keyring, got:", err) } - plainMessage, err := myKeyring.Decrypt(pgpMessage, senderKeyring, 0) + plainMessage, err := issue11Keyring.Decrypt(pgpMessage, senderKeyring, 0) if err != nil { t.Fatal("Expected no error while decrypting/verifying, got:", err) } @@ -140,20 +132,12 @@ func TestIssue11(t *testing.T) { } func TestSignedMessageDecryption(t *testing.T) { - testPrivateKeyRing, err = BuildKeyRingArmored(readTestFile("keyring_privateKey", false)) - - // Password defined in keyring_test - err = testPrivateKeyRing.UnlockWithPassphrase(testMailboxPassword) - if err != nil { - t.Fatal("Expected no error unlocking privateKey, got:", err) - } - pgpMessage, err := NewPGPMessageFromArmored(readTestFile("message_signed", false)) if err != nil { t.Fatal("Expected no error when unarmoring, got:", err) } - decrypted, err := testPrivateKeyRing.Decrypt(pgpMessage, nil, 0) + decrypted, err := keyRingTestPrivate.Decrypt(pgpMessage, nil, 0) if err != nil { t.Fatal("Expected no error when decrypting, got:", err) } @@ -162,24 +146,9 @@ func TestSignedMessageDecryption(t *testing.T) { func TestMultipleKeyMessageEncryption(t *testing.T) { var message = NewPlainMessageFromString("plain text") + assert.Exactly(t, 3, len(keyRingTestMultiple.entities)) - testPublicKeyRing, _ = BuildKeyRingArmored(readTestFile("keyring_publicKey", false)) - err = testPublicKeyRing.ReadFrom(strings.NewReader(readTestFile("mime_publicKey", false)), true) - if err != nil { - t.Fatal("Expected no error adding second public key, got:", err) - } - - assert.Exactly(t, 2, len(testPublicKeyRing.entities)) - - testPrivateKeyRing, err = BuildKeyRingArmored(readTestFile("keyring_privateKey", false)) - - // Password defined in keyring_test - err = testPrivateKeyRing.UnlockWithPassphrase(testMailboxPassword) - if err != nil { - t.Fatal("Expected no error unlocking privateKey, got:", err) - } - - ciphertext, err := testPublicKeyRing.Encrypt(message, testPrivateKeyRing) + ciphertext, err := keyRingTestMultiple.Encrypt(message, keyRingTestPrivate) if err != nil { t.Fatal("Expected no error when encrypting, got:", err) } @@ -189,17 +158,15 @@ func TestMultipleKeyMessageEncryption(t *testing.T) { for { var p packet.Packet if p, err = packets.Next(); err == io.EOF { - err = nil break } - switch p.(type) { - case *packet.EncryptedKey: - numKeyPackets++ + if _, ok := p.(*packet.EncryptedKey); ok { + numKeyPackets++ } } - assert.Exactly(t, 2, numKeyPackets) + assert.Exactly(t, 3, numKeyPackets) - decrypted, err := testPrivateKeyRing.Decrypt(ciphertext, testPublicKeyRing, GetUnixTime()) + decrypted, err := keyRingTestPrivate.Decrypt(ciphertext, keyRingTestPublic, GetUnixTime()) if err != nil { t.Fatal("Expected no error when decrypting, got:", err) } diff --git a/crypto/mime.go b/crypto/mime.go index fcdcdfd..9210252 100644 --- a/crypto/mime.go +++ b/crypto/mime.go @@ -77,7 +77,7 @@ func parseMIME( err = gomime.VisitAll(bytes.NewReader(mmBodyData), h, signatureCollector) if err == nil && verifierKey != nil { - err = signatureCollector.verified; + err = signatureCollector.verified } return bodyCollector, diff --git a/crypto/mime_test.go b/crypto/mime_test.go index 2b37efb..a8c1966 100644 --- a/crypto/mime_test.go +++ b/crypto/mime_test.go @@ -7,7 +7,7 @@ import ( ) // Corresponding key in testdata/mime_privateKey -const privateKeyPassword = "test" +var MIMEKeyPassword = []byte("test") // define call back interface type Callbacks struct { @@ -37,13 +37,22 @@ func TestDecrypt(t *testing.T) { callbacks := Callbacks{ Testing: t, } - privateKeyRing, _ := BuildKeyRingArmored(readTestFile("mime_privateKey", false)) - err = privateKeyRing.UnlockWithPassphrase(privateKeyPassword) + privateKey, err := NewKeyFromArmored(readTestFile("mime_privateKey", false)) + if err != nil { + t.Fatal("Cannot unarmor private key:", err) + } + + privateKey, err = privateKey.Unlock(MIMEKeyPassword) if err != nil { t.Fatal("Cannot unlock private key:", err) } + privateKeyRing, err := 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) @@ -60,7 +69,7 @@ func TestParse(t *testing.T) { body, atts, attHeaders, err := parseMIME(readTestFile("mime_testMessage", false), nil) if err != nil { - t.Error("Expected no error while parsing message, got:", err) + t.Fatal("Expected no error while parsing message, got:", err) } _ = atts diff --git a/crypto/password.go b/crypto/password.go new file mode 100644 index 0000000..8230626 --- /dev/null +++ b/crypto/password.go @@ -0,0 +1,151 @@ +package crypto + +import ( + "bytes" + "io" + + "golang.org/x/crypto/openpgp" + "golang.org/x/crypto/openpgp/packet" + + "github.com/pkg/errors" +) + +// Encrypt 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.GetBinary(), password) + if err != nil { + return nil, err + } + + return NewPGPMessage(encrypted), nil +} + +// Decrypt 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) { + decrypted, err := passwordDecrypt(message.NewReader(), password) + if err != nil { + return nil, err + } + + binMessage := NewPlainMessage(decrypted) + return binMessage, 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 { + return &SessionKey{ + Key: key, + Algo: getAlgo(cipherFunc), + }, nil + } + } + } + + return nil, errors.New("gopenpgp: password incorrect") +} + +// 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") + } + + 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 []byte, password []byte) ([]byte, error) { + var outBuf bytes.Buffer + + config := &packet.Config{ + Time: getTimeGenerator(), + } + + encryptWriter, err := openpgp.SymmetricallyEncrypt(&outBuf, password, nil, config) + if err != nil { + return nil, err + } + _, err = encryptWriter.Write(message) + if err != nil { + return nil, err + } + + err = encryptWriter.Close() + if err != nil { + return nil, err + } + + return outBuf.Bytes(), nil +} + +func passwordDecrypt(encryptedIO io.Reader, password []byte) ([]byte, error) { + firstTimeCalled := true + var prompt = func(keys []openpgp.Key, symmetric bool) ([]byte, error) { + if firstTimeCalled { + firstTimeCalled = false + return password, nil + } + return nil, errors.New("gopenpgp: wrong password in symmetric decryption") + } + + config := &packet.Config{ + Time: getTimeGenerator(), + } + md, err := openpgp.ReadMessage(encryptedIO, nil, prompt, config) + if err != nil { + return nil, err + } + + messageBuf := bytes.NewBuffer(nil) + _, err = io.Copy(messageBuf, md.UnverifiedBody) + if err != nil { + return nil, err + } + + return messageBuf.Bytes(), nil +} diff --git a/crypto/session_test.go b/crypto/session_test.go deleted file mode 100644 index 5e646fd..0000000 --- a/crypto/session_test.go +++ /dev/null @@ -1,74 +0,0 @@ -package crypto - -import ( - "strings" - "testing" - - "github.com/ProtonMail/gopenpgp/constants" - "github.com/stretchr/testify/assert" -) - -var testRandomToken []byte - -func TestRandomToken(t *testing.T) { - var err error - testRandomToken, err = RandomToken() - if err != nil { - t.Fatal("Expected no error while generating default length random token, got:", err) - } - - token40, err := RandomTokenSize(40) - if err != nil { - t.Fatal("Expected no error while generating random token, got:", err) - } - - assert.Len(t, testRandomToken, 32) - assert.Len(t, token40, 40) -} - -func TestAsymmetricKeyPacket(t *testing.T) { - symmetricKey := &SymmetricKey{ - Key: testRandomToken, - Algo: constants.AES256, - } - - privateKeyRing, _ := ReadArmoredKeyRing(strings.NewReader(readTestFile("keyring_privateKey", false))) - _ = privateKeyRing.UnlockWithPassphrase(testMailboxPassword) - - keyPacket, err := privateKeyRing.EncryptSessionKey(symmetricKey) - if err != nil { - t.Fatal("Expected no error while generating key packet, got:", err) - } - - // Password defined in keyring_test - outputSymmetricKey, err := privateKeyRing.DecryptSessionKey(keyPacket) - if err != nil { - t.Fatal("Expected no error while decrypting key packet, got:", err) - } - - assert.Exactly(t, symmetricKey, outputSymmetricKey) -} - -func TestSymmetricKeyPacket(t *testing.T) { - symmetricKey := &SymmetricKey{ - Key: testRandomToken, - Algo: constants.AES256, - } - - password := "I like encryption" - - keyPacket, err := symmetricKey.EncryptToKeyPacket(password) - if err != nil { - t.Fatal("Expected no error while generating key packet, got:", err) - } - - _, err = NewSymmetricKeyFromKeyPacket(keyPacket, "Wrong password") - assert.EqualError(t, err, "gopenpgp: password incorrect") - - outputSymmetricKey, err := NewSymmetricKeyFromKeyPacket(keyPacket, password) - if err != nil { - t.Fatal("Expected no error while decrypting key packet, got:", err) - } - - assert.Exactly(t, symmetricKey, outputSymmetricKey) -} diff --git a/crypto/sessionkey.go b/crypto/sessionkey.go new file mode 100644 index 0000000..c6ac54f --- /dev/null +++ b/crypto/sessionkey.go @@ -0,0 +1,221 @@ +package crypto + +import ( + "bytes" + "encoding/base64" + "fmt" + "io" + + "github.com/ProtonMail/gopenpgp/v2/constants" + "github.com/pkg/errors" + + "golang.org/x/crypto/openpgp" + "golang.org/x/crypto/openpgp/packet" +) + +// SessionKey stores a decrypted session key. +type SessionKey struct { + // The decrypted binary session key. + Key []byte + // The symmetric encryption algorithm used with this key. + Algo string +} + +var symKeyAlgos = map[string]packet.CipherFunction{ + constants.ThreeDES: packet.Cipher3DES, + constants.TripleDES: packet.Cipher3DES, + constants.CAST5: packet.CipherCAST5, + constants.AES128: packet.CipherAES128, + constants.AES192: packet.CipherAES192, + constants.AES256: packet.CipherAES256, +} + +// GetCipherFunc returns the cipher function corresponding to the algorithm used +// with this SessionKey. +func (sk *SessionKey) GetCipherFunc() (packet.CipherFunction, error) { + cf, ok := symKeyAlgos[sk.Algo] + if !ok { + return cf, errors.New("gopenpgp: unsupported cipher function: " + sk.Algo) + } + return cf, nil +} + +// GetBase64Key returns the session key as base64 encoded string. +func (sk *SessionKey) GetBase64Key() string { + return base64.StdEncoding.EncodeToString(sk.Key) +} + +// RandomToken generates a random token with the specified key size +func RandomToken(size int) ([]byte, error) { + config := &packet.Config{DefaultCipher: packet.CipherAES256} + symKey := make([]byte, size) + if _, err := io.ReadFull(config.Random(), symKey); err != nil { + return nil, err + } + return symKey, nil +} + +// GenerateSessionKeyAlgo generates a random key of the correct length for the specified algorithm +func GenerateSessionKeyAlgo(algo string) (sk *SessionKey, err error) { + cf, ok := symKeyAlgos[algo] + if !ok { + return nil, errors.New("gopenpgp: unknown symmetric key generation algorithm") + } + r, err := RandomToken(cf.KeySize()) + if err != nil { + return nil, err + } + + sk = &SessionKey{ + Key: r, + Algo: algo, + } + return sk, nil +} + +// GenerateSessionKey generates a random key for the default cipher +func GenerateSessionKey() (*SessionKey, error) { + return GenerateSessionKeyAlgo(constants.AES256) +} + +func NewSessionKeyFromToken(token []byte, algo string) *SessionKey { + return &SessionKey{ + Key: token, + Algo: algo, + } +} + +func newSessionKeyFromEncrypted(ek *packet.EncryptedKey) (*SessionKey, error) { + var algo string + for k, v := range symKeyAlgos { + if v == ek.CipherFunc { + algo = k + break + } + } + if algo == "" { + return nil, fmt.Errorf("gopenpgp: unsupported cipher function: %v", ek.CipherFunc) + } + + symmetricKey := &SessionKey{ + Key: ek.Key, + Algo: algo, + } + + return symmetricKey, 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) { + var encBuf bytes.Buffer + var encryptWriter io.WriteCloser + + dc, err := sk.GetCipherFunc() + if err != nil { + return nil, errors.Wrap(err, "gopenpgp: unable to encrypt with session key") + } + + config := &packet.Config{ + Time: getTimeGenerator(), + DefaultCipher: dc, + } + + encryptWriter, err = packet.SerializeSymmetricallyEncrypted(&encBuf, config.Cipher(), sk.Key, config) + if err != nil { + return 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, errors.Wrap(err, "gopenpgp: error in compression") + } + } + + encryptWriter, err = packet.SerializeLiteral(encryptWriter, false, "", 0) + if err != nil { + return nil, errors.Wrap(err, "gopenpgp: unable to serialize") + } + + _, err = encryptWriter.Write(message.GetBinary()) + if err != nil { + return nil, err + } + + err = encryptWriter.Close() + if err != nil { + return nil, err + } + + return encBuf.Bytes(), nil +} + +// Decrypt decrypts password protected pgp binary messages +// * encrypted: PGPMessage +// * output: PlainMessage +func (sk *SessionKey) Decrypt(dataPacket []byte) (*PlainMessage, error) { + var messageReader = bytes.NewReader(dataPacket) + var decrypted io.ReadCloser + var decBuf bytes.Buffer + + // 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: + dc, err := sk.GetCipherFunc() + if err != nil { + return nil, errors.Wrap(err, "gopenpgp: unable to decrypt with session key") + } + + decrypted, err = p.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") + } + _, err = decBuf.ReadFrom(decrypted) + if err != nil { + return nil, errors.Wrap(err, "gopenpgp: unable to read from decrypted symmetric packet") + } + + config := &packet.Config{ + Time: getTimeGenerator(), + } + + // Push decrypted packet as literal packet and use openpgp's reader + keyring := openpgp.EntityList{} // Ignore signatures, since we have no private key + md, err := openpgp.ReadMessage(&decBuf, keyring, nil, config) + if err != nil { + return nil, errors.Wrap(err, "gopenpgp: unable to decode symmetric packet") + } + + messageBuf := new(bytes.Buffer) + _, err = messageBuf.ReadFrom(md.UnverifiedBody) + if err != nil { + return nil, err + } + + return NewPlainMessage(messageBuf.Bytes()), nil +} + +func getAlgo(cipher packet.CipherFunction) string { + algo := constants.AES256 + for k, v := range symKeyAlgos { + if v == cipher { + algo = k + break + } + } + + return algo +} diff --git a/crypto/sessionkey_test.go b/crypto/sessionkey_test.go new file mode 100644 index 0000000..2f0b015 --- /dev/null +++ b/crypto/sessionkey_test.go @@ -0,0 +1,145 @@ +package crypto + +import ( + "testing" + + "github.com/ProtonMail/gopenpgp/v2/constants" + "github.com/stretchr/testify/assert" +) + +var testSessionKey *SessionKey + +func init() { + var err error + testSessionKey, err = GenerateSessionKey() + if err != nil { + panic("Expected no error while generating random session key with default algorithm, got:" + err.Error()) + } +} + +func TestRandomToken(t *testing.T) { + token40, err := RandomToken(40) + if err != nil { + t.Fatal("Expected no error while generating random token, got:", err) + } + assert.Len(t, token40, 40) +} + +func TestGenerateSessionKey(t *testing.T) { + assert.Len(t, testSessionKey.Key, 32) +} + +func TestAsymmetricKeyPacket(t *testing.T) { + keyPacket, err := keyRingTestPublic.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) + if err != nil { + t.Fatal("Expected no error while decrypting key packet, got:", err) + } + + assert.Exactly(t, testSessionKey, outputSymmetricKey) +} + +func TestSymmetricKeyPacket(t *testing.T) { + password := []byte("I like encryption") + + keyPacket, err := EncryptSessionKeyWithPassword(testSessionKey, password) + if err != nil { + t.Fatal("Expected no error while generating key packet, got:", err) + } + + _, err = DecryptSessionKeyWithPassword(keyPacket, []byte("Wrong password")) + assert.EqualError(t, err, "gopenpgp: password incorrect") + + outputSymmetricKey, err := DecryptSessionKeyWithPassword(keyPacket, password) + if err != nil { + t.Fatal("Expected no error while decrypting key packet, got:", err) + } + + assert.Exactly(t, testSessionKey, outputSymmetricKey) +} + +func TestDataPacketEncryption(t *testing.T) { + var message = NewPlainMessageFromString("The secret code is... 1, 2, 3, 4, 5") + + // Encrypt data with session key + dataPacket, err := testSessionKey.Encrypt(message) + if err != nil { + t.Fatal("Expected no error when encrypting, got:", err) + } + // Decrypt data with wrong session key + wrongKey := SessionKey{ + Key: []byte("wrong pass"), + Algo: constants.AES256, + } + _, err = wrongKey.Decrypt(dataPacket) + assert.NotNil(t, err) + + // 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()) + + // Encrypt session key + keyPacket, err := keyRingTestPublic.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) + + // Armor and un-armor message. In alternative it can also be done with NewPgpMessage(splitMessage.GetBinary()) + armored, err := splitMessage.GetArmored() + if err != nil { + t.Fatal("Unable to armor split message, got:", err) + } + + pgpMessage, err := NewPGPMessageFromArmored(armored) + if err != nil { + t.Fatal("Unable to unarmor pgp message, got:", err) + } + + // Test if final decryption succeeds + finalMessage, err := keyRingTestPrivate.Decrypt(pgpMessage, nil, 0) + if err != nil { + t.Fatal("Unable to decrypt joined keypacket and datapacket, got:", err) + } + + assert.Exactly(t, message.GetString(), finalMessage.GetString()) +} + +func TestDataPacketDecryption(t *testing.T) { + pgpMessage, err := NewPGPMessageFromArmored(readTestFile("message_signed", false)) + if err != nil { + t.Fatal("Expected no error when unarmoring, got:", err) + } + + split, err := pgpMessage.SeparateKeyAndData(1024, 0) + if err != nil { + t.Fatal("Expected no error when splitting, got:", err) + } + + sessionKey, err := keyRingTestPrivate.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, readTestFile("message_plaintext", true), decrypted.GetString()) +} + +func TestSessionKeyClear(t *testing.T) { + testSessionKey.Clear() + assertMemCleared(t, testSessionKey.Key) +} diff --git a/crypto/signature.go b/crypto/signature.go index cfd4cf1..120731d 100644 --- a/crypto/signature.go +++ b/crypto/signature.go @@ -8,16 +8,16 @@ import ( "time" "golang.org/x/crypto/openpgp" - "golang.org/x/crypto/openpgp/packet" pgpErrors "golang.org/x/crypto/openpgp/errors" + "golang.org/x/crypto/openpgp/packet" - "github.com/ProtonMail/gopenpgp/constants" - "github.com/ProtonMail/gopenpgp/internal" + "github.com/ProtonMail/gopenpgp/v2/constants" + "github.com/ProtonMail/gopenpgp/v2/internal" ) // SignatureVerificationError is returned from Decrypt and VerifyDetached functions when signature verification fails type SignatureVerificationError struct { - Status int + Status int Message string } @@ -32,7 +32,7 @@ func (e SignatureVerificationError) Error() string { // newSignatureFailed creates a new SignatureVerificationError, type SIGNATURE_FAILED func newSignatureFailed() SignatureVerificationError { - return SignatureVerificationError { + return SignatureVerificationError{ constants.SIGNATURE_FAILED, "Invalid signature", } @@ -40,7 +40,7 @@ func newSignatureFailed() SignatureVerificationError { // newSignatureNotSigned creates a new SignatureVerificationError, type SIGNATURE_NOT_SIGNED func newSignatureNotSigned() SignatureVerificationError { - return SignatureVerificationError { + return SignatureVerificationError{ constants.SIGNATURE_NOT_SIGNED, "Missing signature", } @@ -48,7 +48,7 @@ func newSignatureNotSigned() SignatureVerificationError { // newSignatureNoVerifier creates a new SignatureVerificationError, type SIGNATURE_NO_VERIFIER func newSignatureNoVerifier() SignatureVerificationError { - return SignatureVerificationError { + return SignatureVerificationError{ constants.SIGNATURE_NO_VERIFIER, "No matching signature", } diff --git a/crypto/signature_collector.go b/crypto/signature_collector.go index 4e35012..47459ff 100644 --- a/crypto/signature_collector.go +++ b/crypto/signature_collector.go @@ -85,7 +85,8 @@ func (sc *SignatureCollector) Accept( if err != nil { return err } - buffer, err = gomime.DecodeCharset(buffer, params) + mediaType, _, _ := mime.ParseMediaType(header.Get("Content-Type")) + buffer, err = gomime.DecodeCharset(buffer, mediaType, params) if err != nil { return err } diff --git a/crypto/signature_test.go b/crypto/signature_test.go index c46569c..c69869e 100644 --- a/crypto/signature_test.go +++ b/crypto/signature_test.go @@ -2,39 +2,23 @@ package crypto import ( "regexp" - "strings" "testing" - "github.com/ProtonMail/gopenpgp/constants" + "github.com/ProtonMail/gopenpgp/v2/constants" "github.com/stretchr/testify/assert" ) const signedPlainText = "Signed message\n" -const testTime = 1557754627 // 2019-05-13T13:37:07+00:00 -var signingKeyRing *KeyRing var textSignature, binSignature *PGPSignature var message *PlainMessage var signatureTest = regexp.MustCompile("(?s)^-----BEGIN PGP SIGNATURE-----.*-----END PGP SIGNATURE-----$") -var signedMessageTest = regexp.MustCompile( - "(?s)^-----BEGIN PGP SIGNED MESSAGE-----.*-----BEGIN PGP SIGNATURE-----.*-----END PGP SIGNATURE-----$") func TestSignTextDetached(t *testing.T) { var err error - signingKeyRing, err = ReadArmoredKeyRing(strings.NewReader(readTestFile("keyring_privateKey", false))) - if err != nil { - t.Fatal("Cannot read private key:", err) - } - - // Password defined in keyring_test - err = signingKeyRing.UnlockWithPassphrase(testMailboxPassword) - if err != nil { - t.Fatal("Cannot decrypt private key:", err) - } - message = NewPlainMessageFromString(signedPlainText) - textSignature, err = signingKeyRing.SignDetached(message) + textSignature, err = keyRingTestPrivate.SignDetached(message) if err != nil { t.Fatal("Cannot generate signature:", err) } @@ -48,15 +32,15 @@ func TestSignTextDetached(t *testing.T) { } func TestVerifyTextDetachedSig(t *testing.T) { - verificationError := signingKeyRing.VerifyDetached(message, textSignature, testTime) + verificationError := keyRingTestPublic.VerifyDetached(message, textSignature, testTime) if verificationError != nil { - t.Fatal("Cannot verify plaintext signature:", err) + t.Fatal("Cannot verify plaintext signature:", verificationError) } } func TestVerifyTextDetachedSigWrong(t *testing.T) { fakeMessage := NewPlainMessageFromString("wrong text") - verificationError := signingKeyRing.VerifyDetached(fakeMessage, textSignature, testTime) + verificationError := keyRingTestPublic.VerifyDetached(fakeMessage, textSignature, testTime) assert.EqualError(t, verificationError, "Signature Verification Error: Invalid signature") @@ -67,7 +51,7 @@ func TestVerifyTextDetachedSigWrong(t *testing.T) { func TestSignBinDetached(t *testing.T) { var err error - binSignature, err = signingKeyRing.SignDetached(NewPlainMessage([]byte(signedPlainText))) + binSignature, err = keyRingTestPrivate.SignDetached(NewPlainMessage([]byte(signedPlainText))) if err != nil { t.Fatal("Cannot generate signature:", err) } @@ -81,8 +65,8 @@ func TestSignBinDetached(t *testing.T) { } func TestVerifyBinDetachedSig(t *testing.T) { - verificationError := signingKeyRing.VerifyDetached(message, binSignature, testTime) + verificationError := keyRingTestPublic.VerifyDetached(message, binSignature, testTime) if verificationError != nil { - t.Fatal("Cannot verify binary signature:", err) + t.Fatal("Cannot verify binary signature:", verificationError) } } diff --git a/crypto/symmetrickey.go b/crypto/symmetrickey.go deleted file mode 100644 index 8761626..0000000 --- a/crypto/symmetrickey.go +++ /dev/null @@ -1,225 +0,0 @@ -package crypto - -import ( - "bytes" - "encoding/base64" - "errors" - "fmt" - "io" - - "github.com/ProtonMail/gopenpgp/constants" - - "golang.org/x/crypto/openpgp" - "golang.org/x/crypto/openpgp/packet" -) - -// SymmetricKey stores a decrypted session key. -type SymmetricKey struct { - // The decrypted binary session key. - Key []byte - // The symmetric encryption algorithm used with this key. - Algo string -} - -var symKeyAlgos = map[string]packet.CipherFunction{ - constants.ThreeDES: packet.Cipher3DES, - constants.TripleDES: packet.Cipher3DES, - constants.CAST5: packet.CipherCAST5, - constants.AES128: packet.CipherAES128, - constants.AES192: packet.CipherAES192, - constants.AES256: packet.CipherAES256, -} - -// GetCipherFunc returns the cipher function corresponding to the algorithm used -// with this SymmetricKey. -func (symmetricKey *SymmetricKey) GetCipherFunc() packet.CipherFunction { - cf, ok := symKeyAlgos[symmetricKey.Algo] - if ok { - return cf - } - - panic("gopenpgp: unsupported cipher function: " + symmetricKey.Algo) -} - -// GetBase64Key returns the session key as base64 encoded string. -func (symmetricKey *SymmetricKey) GetBase64Key() string { - return base64.StdEncoding.EncodeToString(symmetricKey.Key) -} - -func NewSymmetricKeyFromToken(passphrase, algo string) *SymmetricKey { - return &SymmetricKey{ - Key: []byte(passphrase), - Algo: algo, - } -} - -func newSymmetricKeyFromEncrypted(ek *packet.EncryptedKey) (*SymmetricKey, error) { - var algo string - for k, v := range symKeyAlgos { - if v == ek.CipherFunc { - algo = k - break - } - } - if algo == "" { - return nil, fmt.Errorf("gopenpgp: unsupported cipher function: %v", ek.CipherFunc) - } - - symmetricKey := &SymmetricKey{ - Key: ek.Key, - Algo: algo, - } - - return symmetricKey, nil -} - -// Encrypt encrypts a PlainMessage to PGPMessage with a SymmetricKey -// * message : The plain data as a PlainMessage -// * output : The encrypted data as PGPMessage -func (symmetricKey *SymmetricKey) Encrypt(message *PlainMessage) (*PGPMessage, error) { - encrypted, err := symmetricEncrypt(message.GetBinary(), symmetricKey) - if err != nil { - return nil, err - } - - return NewPGPMessage(encrypted), nil -} - -// Decrypt decrypts password protected pgp binary messages -// * encrypted: PGPMessage -// * output: PlainMessage -func (symmetricKey *SymmetricKey) Decrypt(message *PGPMessage) (*PlainMessage, error) { - decrypted, err := symmetricDecrypt(message.NewReader(), symmetricKey) - if err != nil { - return nil, err - } - - binMessage := NewPlainMessage(decrypted) - return binMessage, nil -} - -// NewSymmetricKeyFromKeyPacket decrypts the binary symmetrically encrypted -// session key packet and returns the session key. -func NewSymmetricKeyFromKeyPacket(keyPacket []byte, password string) (*SymmetricKey, 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 - } - - switch p := p.(type) { - case *packet.SymmetricKeyEncrypted: - symKeys = append(symKeys, p) - } - } - - pwdRaw := []byte(password) - // Try the symmetric passphrase first - if len(symKeys) != 0 && pwdRaw != nil { - for _, s := range symKeys { - key, cipherFunc, err := s.Decrypt(pwdRaw) - if err == nil { - return &SymmetricKey{ - Key: key, - Algo: getAlgo(cipherFunc), - }, nil - } - - } - } - - return nil, errors.New("gopenpgp: password incorrect") -} - -// EncryptToKeyPacket encrypts the session key with the password and -// returns a binary symmetrically encrypted session key packet. -func (symmetricKey *SymmetricKey) EncryptToKeyPacket(password string) ([]byte, error) { - outbuf := &bytes.Buffer{} - - cf := symmetricKey.GetCipherFunc() - - if len(password) <= 0 { - return nil, errors.New("gopenpgp: password can't be empty") - } - - pwdRaw := []byte(password) - - config := &packet.Config{ - DefaultCipher: cf, - } - - err := packet.SerializeSymmetricKeyEncryptedReuseKey(outbuf, symmetricKey.Key, pwdRaw, config) - if err != nil { - return nil, err - } - return outbuf.Bytes(), nil -} - -// ----- INTERNAL FUNCTIONS ------ - -func symmetricEncrypt(message []byte, sk *SymmetricKey) ([]byte, error) { - var outBuf bytes.Buffer - - config := &packet.Config{ - Time: getTimeGenerator(), - DefaultCipher: sk.GetCipherFunc(), - } - - encryptWriter, err := openpgp.SymmetricallyEncrypt(&outBuf, sk.Key, nil, config) - if err != nil { - return nil, err - } - _, err = encryptWriter.Write(message) - encryptWriter.Close() - - if err != nil { - return nil, err - } - - return outBuf.Bytes(), nil -} - -func symmetricDecrypt(encryptedIO io.Reader, sk *SymmetricKey) ([]byte, error) { - firstTimeCalled := true - var prompt = func(keys []openpgp.Key, symmetric bool) ([]byte, error) { - if firstTimeCalled { - firstTimeCalled = false - return sk.Key, nil - } - return nil, errors.New("gopenpgp: wrong password in symmetric decryption") - } - - config := &packet.Config{ - Time: getTimeGenerator(), - } - md, err := openpgp.ReadMessage(encryptedIO, nil, prompt, config) - if err != nil { - return nil, err - } - - messageBuf := bytes.NewBuffer(nil) - _, err = io.Copy(messageBuf, md.UnverifiedBody) - if err != nil { - return nil, err - } - - return messageBuf.Bytes(), nil -} - -func getAlgo(cipher packet.CipherFunction) string { - algo := constants.AES256 - for k, v := range symKeyAlgos { - if v == cipher { - algo = k - break - } - } - - return algo -} diff --git a/crypto/time.go b/crypto/time.go index 1795395..06aa3d2 100644 --- a/crypto/time.go +++ b/crypto/time.go @@ -1,8 +1,8 @@ package crypto import ( - "time" "errors" + "time" ) // UpdateTime updates cached time @@ -31,7 +31,7 @@ func getNow() time.Time { return time.Now() } - return time.Unix(pgp.latestServerTime + extrapolate, 0) + return time.Unix(pgp.latestServerTime+extrapolate, 0) } func getDiff() (int64, error) { @@ -40,12 +40,10 @@ func getDiff() (int64, error) { return int64(time.Since(pgp.latestClientTime).Seconds()), nil } - return 0, errors.New("Latest server time not available") + return 0, errors.New("gopenpgp: latest server time not available") } // getTimeGenerator Returns a time generator function func getTimeGenerator() func() time.Time { - return func() time.Time { - return getNow() - } + return getNow } diff --git a/go.mod b/go.mod index 1519529..1793e28 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,12 @@ -module github.com/ProtonMail/gopenpgp +module github.com/ProtonMail/gopenpgp/v2 go 1.12 require ( - github.com/ProtonMail/go-mime v0.0.0-20190521135552-09454e3dbe72 - github.com/stretchr/testify v1.2.2 - golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f + github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a + github.com/pkg/errors v0.8.1 + github.com/stretchr/testify v1.4.0 + golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392 ) -replace golang.org/x/crypto => github.com/ProtonMail/crypto v0.0.0-20190814153124-b5b07a6add54 +replace golang.org/x/crypto => github.com/ProtonMail/crypto v0.0.0-20191122234321-e77a1f03baa0 diff --git a/go.sum b/go.sum index 938b99f..b1cb6b7 100644 --- a/go.sum +++ b/go.sum @@ -1,25 +1,31 @@ -github.com/ProtonMail/crypto v0.0.0-20190814153124-b5b07a6add54 h1:b9Mgk9zYaSxsqeaq/qCUsPBIR95BcyjzTL+uFoPBG1o= -github.com/ProtonMail/crypto v0.0.0-20190814153124-b5b07a6add54/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -github.com/ProtonMail/go-mime v0.0.0-20190521135552-09454e3dbe72 h1:hGCc4Oc2fD3I5mNnZ1VlREncVc9EXJF8dxW3sw16gWM= -github.com/ProtonMail/go-mime v0.0.0-20190521135552-09454e3dbe72/go.mod h1:NYt+V3/4rEeDuaev/zw1zCq8uqVEuPHzDPo3OZrlGJ4= +github.com/ProtonMail/crypto v0.0.0-20191122234321-e77a1f03baa0 h1:mCww5Yl0Pm4PZPSooupyWDgihrh96p6+O4PY1hs0FBw= +github.com/ProtonMail/crypto v0.0.0-20191122234321-e77a1f03baa0/go.mod h1:MBriIAodHvZ+YvwvMJWCTmseW/LkeVRPWp/iZKvee4g= +github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a h1:W6RrgN/sTxg1msqzFFb+G80MFmpjMw61IU+slm+wln4= +github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a/go.mod h1:NYt+V3/4rEeDuaev/zw1zCq8uqVEuPHzDPo3OZrlGJ4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/helper/base_test.go b/helper/base_test.go index 4791fda..f520c9d 100644 --- a/helper/base_test.go +++ b/helper/base_test.go @@ -3,12 +3,14 @@ package helper import ( "io/ioutil" "strings" + + "github.com/ProtonMail/gopenpgp/v2/crypto" ) -var err error +const testTime = 1557754627 // 2019-05-13T13:37:07+00:00 func readTestFile(name string, trimNewlines bool) string { - data, err := ioutil.ReadFile("../crypto/testdata/" + name) + data, err := ioutil.ReadFile("../crypto/testdata/" + name) //nolint if err != nil { panic(err) } @@ -19,4 +21,8 @@ func readTestFile(name string, trimNewlines bool) string { } // Corresponding key in ../crypto/testdata/keyring_privateKey -const testMailboxPassword = "apple" +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 index a0e40f9..1eab67c 100644 --- a/helper/cleartext.go +++ b/helper/cleartext.go @@ -3,30 +3,40 @@ package helper import ( "strings" - "github.com/ProtonMail/gopenpgp/crypto" - "github.com/ProtonMail/gopenpgp/internal" + "github.com/ProtonMail/gopenpgp/v2/crypto" + "github.com/ProtonMail/gopenpgp/v2/internal" ) // 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, passphrase, text string) (string, error) { - signingKeyRing, err := crypto.BuildKeyRingArmored(privateKey) +func SignCleartextMessageArmored(privateKey string, passphrase []byte, text string) (string, error) { + signingKey, err := crypto.NewKeyFromArmored(privateKey) if err != nil { return "", err } - err = signingKeyRing.UnlockWithPassphrase(passphrase) + unlockedKey, err := signingKey.Unlock(passphrase) if err != nil { return "", err } - return SignCleartextMessage(signingKeyRing, text) + keyRing, err := crypto.NewKeyRing(unlockedKey) + if err != nil { + return "", err + } + + 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) { - verifyKeyRing, err := crypto.BuildKeyRingArmored(publicKey) + signingKey, err := crypto.NewKeyFromArmored(publicKey) + if err != nil { + return "", err + } + + verifyKeyRing, err := crypto.NewKeyRing(signingKey) if err != nil { return "", err } diff --git a/helper/cleartext_test.go b/helper/cleartext_test.go index 5905661..5d8cf89 100644 --- a/helper/cleartext_test.go +++ b/helper/cleartext_test.go @@ -4,12 +4,12 @@ import ( "regexp" "testing" + "github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/stretchr/testify/assert" - "github.com/ProtonMail/gopenpgp/crypto" ) const signedPlainText = "Signed message\n" -const testTime = 1557754627 // 2019-05-13T13:37:07+00:00 + var signedMessageTest = regexp.MustCompile( "(?s)^-----BEGIN PGP SIGNED MESSAGE-----.*-----BEGIN PGP SIGNATURE-----.*-----END PGP SIGNATURE-----$") diff --git a/helper/helper.go b/helper/helper.go index 6939ac5..b8ffd07 100644 --- a/helper/helper.go +++ b/helper/helper.go @@ -1,29 +1,19 @@ +// helper contains several functions with a simple interface to extend usability and compatibility with gomobile package helper import ( "errors" - "github.com/ProtonMail/gopenpgp/constants" - "github.com/ProtonMail/gopenpgp/crypto" + "github.com/ProtonMail/gopenpgp/v2/crypto" ) -// EncryptMessageWithToken encrypts a string with a passphrase using AES256 -func EncryptMessageWithToken( - passphrase, plaintext string, -) (ciphertext string, err error) { - return EncryptMessageWithTokenAlgo(passphrase, plaintext, constants.AES256) -} - -// EncryptMessageWithTokenAlgo encrypts a string with a random token and an algorithm chosen from constants.* -func EncryptMessageWithTokenAlgo( - token, plaintext, algo string, -) (ciphertext string, err error) { +// 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) - var key = crypto.NewSymmetricKeyFromToken(token, algo) - if pgpMessage, err = key.Encrypt(message); err != nil { + if pgpMessage, err = crypto.EncryptMessageWithPassword(message, password); err != nil { return "", err } @@ -34,19 +24,17 @@ func EncryptMessageWithTokenAlgo( return ciphertext, nil } -// DecryptMessageWithToken decrypts an armored message with a random token. +// DecryptMessageWithPassword decrypts an armored message with a random token. // The algorithm is derived from the armoring. -func DecryptMessageWithToken(token, ciphertext string) (plaintext string, err error) { +func DecryptMessageWithPassword(password []byte, ciphertext string) (plaintext string, err error) { var message *crypto.PlainMessage var pgpMessage *crypto.PGPMessage - var key = crypto.NewSymmetricKeyFromToken(token, "") - if pgpMessage, err = crypto.NewPGPMessageFromArmored(ciphertext); err != nil { return "", err } - if message, err = key.Decrypt(pgpMessage); err != nil { + if message, err = crypto.DecryptMessageWithPassword(pgpMessage, password); err != nil { return "", err } @@ -54,13 +42,18 @@ func DecryptMessageWithToken(token, ciphertext string) (plaintext string, err er } // EncryptMessageArmored generates an armored PGP message given a plaintext and an armored public key -func EncryptMessageArmored(publicKey, plaintext string) (ciphertext string, err error) { +func EncryptMessageArmored(key, plaintext string) (ciphertext string, err error) { + var publicKey *crypto.Key var publicKeyRing *crypto.KeyRing var pgpMessage *crypto.PGPMessage var message = crypto.NewPlainMessageFromString(plaintext) - if publicKeyRing, err = crypto.BuildKeyRingArmored(publicKey); err != nil { + if publicKey, err = crypto.NewKeyFromArmored(key); err != nil { + return "", err + } + + if publicKeyRing, err = crypto.NewKeyRing(publicKey); err != nil { return "", err } @@ -78,22 +71,31 @@ func EncryptMessageArmored(publicKey, plaintext string) (ciphertext string, err // 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, passphrase, plaintext string, + publicKey, privateKey string, passphrase []byte, plaintext string, ) (ciphertext string, err error) { + var publicKeyObj, privateKeyObj, unlockedKeyObj *crypto.Key var publicKeyRing, privateKeyRing *crypto.KeyRing var pgpMessage *crypto.PGPMessage var message = crypto.NewPlainMessageFromString(plaintext) - if publicKeyRing, err = crypto.BuildKeyRingArmored(publicKey); err != nil { + if publicKeyObj, err = crypto.NewKeyFromArmored(publicKey); err != nil { return "", err } - if privateKeyRing, err = crypto.BuildKeyRingArmored(privateKey); err != nil { + if publicKeyRing, err = crypto.NewKeyRing(publicKeyObj); err != nil { return "", err } - if err = privateKeyRing.UnlockWithPassphrase(passphrase); err != nil { + if privateKeyObj, err = crypto.NewKeyFromArmored(privateKey); err != nil { + return "", err + } + + if unlockedKeyObj, err = privateKeyObj.Unlock(passphrase); err != nil { + return "", err + } + + if privateKeyRing, err = crypto.NewKeyRing(unlockedKeyObj); err != nil { return "", err } @@ -110,17 +112,22 @@ func EncryptSignMessageArmored( // DecryptMessageArmored decrypts an armored PGP message given a private key and its passphrase func DecryptMessageArmored( - privateKey, passphrase, ciphertext string, + privateKey string, passphrase []byte, ciphertext string, ) (plaintext string, err error) { + var privateKeyObj, privateKeyUnlocked *crypto.Key var privateKeyRing *crypto.KeyRing var pgpMessage *crypto.PGPMessage var message *crypto.PlainMessage - if privateKeyRing, err = crypto.BuildKeyRingArmored(privateKey); err != nil { + if privateKeyObj, err = crypto.NewKeyFromArmored(privateKey); err != nil { return "", err } - if err = privateKeyRing.UnlockWithPassphrase(passphrase); err != nil { + if privateKeyUnlocked, err = privateKeyObj.Unlock(passphrase); err != nil { + return "", err + } + + if privateKeyRing, err = crypto.NewKeyRing(privateKeyUnlocked); err != nil { return "", err } @@ -139,21 +146,30 @@ func DecryptMessageArmored( // and verifies the embedded signature. // Returns the plain data or an error on signature verification failure. func DecryptVerifyMessageArmored( - publicKey, privateKey, passphrase, ciphertext string, + publicKey, privateKey string, passphrase []byte, ciphertext string, ) (plaintext string, err error) { + var publicKeyObj, privateKeyObj, unlockedKeyObj *crypto.Key var publicKeyRing, privateKeyRing *crypto.KeyRing var pgpMessage *crypto.PGPMessage var message *crypto.PlainMessage - if publicKeyRing, err = crypto.BuildKeyRingArmored(publicKey); err != nil { + if publicKeyObj, err = crypto.NewKeyFromArmored(publicKey); err != nil { return "", err } - if privateKeyRing, err = crypto.BuildKeyRingArmored(privateKey); err != nil { + if publicKeyRing, err = crypto.NewKeyRing(publicKeyObj); err != nil { return "", err } - if err = privateKeyRing.UnlockWithPassphrase(passphrase); err != nil { + if privateKeyObj, err = crypto.NewKeyFromArmored(privateKey); err != nil { + return "", err + } + + if unlockedKeyObj, err = privateKeyObj.Unlock(passphrase); err != nil { + return "", err + } + + if privateKeyRing, err = crypto.NewKeyRing(unlockedKeyObj); err != nil { return "", err } @@ -172,24 +188,32 @@ func DecryptVerifyMessageArmored( // and its passphrase, the filename, and the unencrypted file data. // Returns keypacket, dataPacket and unarmored (!) signature separate. func EncryptSignAttachment( - publicKey, privateKey, passphrase, fileName string, - plainData []byte, + publicKey, privateKey string, passphrase []byte, fileName string, plainData []byte, ) (keyPacket, dataPacket, signature []byte, err error) { + var publicKeyObj, privateKeyObj, unlockedKeyObj *crypto.Key var publicKeyRing, privateKeyRing *crypto.KeyRing var packets *crypto.PGPSplitMessage var signatureObj *crypto.PGPSignature var binMessage = crypto.NewPlainMessage(plainData) - if publicKeyRing, err = crypto.BuildKeyRingArmored(publicKey); err != nil { + if publicKeyObj, err = crypto.NewKeyFromArmored(publicKey); err != nil { return nil, nil, nil, err } - if privateKeyRing, err = crypto.BuildKeyRingArmored(privateKey); err != nil { + if publicKeyRing, err = crypto.NewKeyRing(publicKeyObj); err != nil { return nil, nil, nil, err } - if err = privateKeyRing.UnlockWithPassphrase(passphrase); err != nil { + if privateKeyObj, err = crypto.NewKeyFromArmored(privateKey); err != nil { + return nil, nil, nil, err + } + + if unlockedKeyObj, err = privateKeyObj.Unlock(passphrase); err != nil { + return nil, nil, nil, err + } + + if privateKeyRing, err = crypto.NewKeyRing(unlockedKeyObj); err != nil { return nil, nil, nil, err } @@ -208,25 +232,34 @@ func EncryptSignAttachment( // 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, passphrase string, - keyPacket, dataPacket []byte, + publicKey, privateKey string, + passphrase, keyPacket, dataPacket []byte, armoredSignature string, ) (plainData []byte, err error) { + var publicKeyObj, privateKeyObj, unlockedKeyObj *crypto.Key var publicKeyRing, privateKeyRing *crypto.KeyRing var detachedSignature *crypto.PGPSignature var message *crypto.PlainMessage var packets = crypto.NewPGPSplitMessage(keyPacket, dataPacket) - if publicKeyRing, err = crypto.BuildKeyRingArmored(publicKey); err != nil { + if publicKeyObj, err = crypto.NewKeyFromArmored(publicKey); err != nil { return nil, err } - if privateKeyRing, err = crypto.BuildKeyRingArmored(privateKey); err != nil { + if publicKeyRing, err = crypto.NewKeyRing(publicKeyObj); err != nil { return nil, err } - if err = privateKeyRing.UnlockWithPassphrase(passphrase); err != nil { + if privateKeyObj, err = crypto.NewKeyFromArmored(privateKey); err != nil { + return nil, err + } + + if unlockedKeyObj, err = privateKeyObj.Unlock(passphrase); err != nil { + return nil, err + } + + if privateKeyRing, err = crypto.NewKeyRing(unlockedKeyObj); err != nil { return nil, err } diff --git a/helper/helper_test.go b/helper/helper_test.go index c9141a4..b79feee 100644 --- a/helper/helper_test.go +++ b/helper/helper_test.go @@ -3,23 +3,23 @@ package helper import ( "testing" - "github.com/ProtonMail/gopenpgp/crypto" + "github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/stretchr/testify/assert" ) func TestAESEncryption(t *testing.T) { var plaintext = "Symmetric secret" - var passphrase = "passphrase" + var passphrase = []byte("passphrase") - ciphertext, err := EncryptMessageWithToken(passphrase, plaintext) + ciphertext, err := EncryptMessageWithPassword(passphrase, plaintext) if err != nil { t.Fatal("Expected no error when encrypting, got:", err) } - _, err = DecryptMessageWithToken("Wrong passphrase", ciphertext) + _, err = DecryptMessageWithPassword([]byte("Wrong passphrase"), ciphertext) assert.EqualError(t, err, "gopenpgp: wrong password in symmetric decryption") - decrypted, err := DecryptMessageWithToken(passphrase, ciphertext) + decrypted, err := DecryptMessageWithPassword(passphrase, ciphertext) if err != nil { t.Fatal("Expected no error when decrypting, got:", err) } diff --git a/helper/ios.go b/helper/ios.go index 22a433d..654858a 100644 --- a/helper/ios.go +++ b/helper/ios.go @@ -1,15 +1,16 @@ package helper import ( - "github.com/ProtonMail/gopenpgp/crypto" + "github.com/ProtonMail/gopenpgp/v2/crypto" ) +// ExplicitVerifyMessage contains explicitly the signature verification error, for gomobile users type ExplicitVerifyMessage struct { - Message *crypto.PlainMessage + Message *crypto.PlainMessage SignatureVerificationError *crypto.SignatureVerificationError } -// DecryptVerifyMessageArmored decrypts an armored PGP message given a private key and its passphrase +// DecryptExplicitVerify 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 DecryptExplicitVerify( @@ -19,7 +20,7 @@ func DecryptExplicitVerify( ) (*ExplicitVerifyMessage, error) { var explicitVerify *ExplicitVerifyMessage - message, err := privateKeyRing.Decrypt(pgpMessage, publicKeyRing, verifyTime); + message, err := privateKeyRing.Decrypt(pgpMessage, publicKeyRing, verifyTime) if err != nil { castedErr, isType := err.(crypto.SignatureVerificationError) @@ -28,12 +29,12 @@ func DecryptExplicitVerify( } explicitVerify = &ExplicitVerifyMessage{ - Message: message, + Message: message, SignatureVerificationError: &castedErr, } } else { explicitVerify = &ExplicitVerifyMessage{ - Message: message, + Message: message, SignatureVerificationError: nil, } } diff --git a/helper/ios_test.go b/helper/ios_test.go index 6ee7b11..c1ae644 100644 --- a/helper/ios_test.go +++ b/helper/ios_test.go @@ -3,20 +3,22 @@ package helper import ( "testing" - "github.com/ProtonMail/gopenpgp/constants" - "github.com/ProtonMail/gopenpgp/crypto" + "github.com/ProtonMail/gopenpgp/v2/constants" + "github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/stretchr/testify/assert" ) func TestIOSSignedMessageDecryption(t *testing.T) { - testPrivateKeyRing, _ := crypto.BuildKeyRingArmored(readTestFile("keyring_privateKey", false)) - testPublicKeyRing, _ := crypto.BuildKeyRingArmored(readTestFile("mime_publicKey", false)) - + privateKey, _ := crypto.NewKeyFromArmored(readTestFile("keyring_privateKey", false)) // Password defined in base_test - err := testPrivateKeyRing.UnlockWithPassphrase(testMailboxPassword) + 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 { @@ -31,7 +33,8 @@ func TestIOSSignedMessageDecryption(t *testing.T) { assert.Exactly(t, constants.SIGNATURE_NO_VERIFIER, decrypted.SignatureVerificationError.Status) assert.Exactly(t, readTestFile("message_plaintext", true), decrypted.Message.GetString()) - testPublicKeyRing, _ = crypto.BuildKeyRingArmored(readTestFile("keyring_publicKey", false)) + publicKey, _ = crypto.NewKeyFromArmored(readTestFile("keyring_publicKey", false)) + testPublicKeyRing, _ = crypto.NewKeyRing(publicKey) pgpMessage, err = testPublicKeyRing.Encrypt(decrypted.Message, testPrivateKeyRing) if err != nil { diff --git a/helper/key.go b/helper/key.go new file mode 100644 index 0000000..8d1d0d6 --- /dev/null +++ b/helper/key.go @@ -0,0 +1,48 @@ +package helper + +import ( + "github.com/ProtonMail/gopenpgp/v2/crypto" +) + +// 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 "", err + } + + unlocked, err := key.Unlock(oldPassphrase) + if err != nil { + return "", err + } + + locked, err := unlocked.Lock(newPassphrase) + if err != nil { + return "", err + } + + unlocked.ClearPrivateParams() + return locked.Armor() +} + +// 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 "", err + } + + locked, err := key.Lock(passphrase) + if err != nil { + return "", err + } + + key.ClearPrivateParams() + return locked.Armor() +} diff --git a/internal/armor.go b/internal/armor.go index 9788e30..1b17df3 100644 --- a/internal/armor.go +++ b/internal/armor.go @@ -1,8 +1,9 @@ package internal import ( - "golang.org/x/crypto/openpgp/armor" "strings" + + "golang.org/x/crypto/openpgp/armor" ) // Unarmor unarmors an armored string. diff --git a/internal/common.go b/internal/common.go index b372807..d329729 100644 --- a/internal/common.go +++ b/internal/common.go @@ -4,7 +4,7 @@ package internal import ( "regexp" - "github.com/ProtonMail/gopenpgp/constants" + "github.com/ProtonMail/gopenpgp/v2/constants" ) // TrimNewlines removes whitespace from the end of each line of the input diff --git a/subtle/subtle.go b/subtle/subtle.go index 5da5b3c..80cb293 100644 --- a/subtle/subtle.go +++ b/subtle/subtle.go @@ -28,8 +28,8 @@ func DecryptWithoutIntegrity(key, input, iv []byte) ([]byte, error) { return EncryptWithoutIntegrity(key, input, iv) } -// DeriveKey derives a key from a password using scrypt. N should be set to the +// 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) +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 index ee97c69..e253b24 100644 --- a/subtle/subtle_test.go +++ b/subtle/subtle_test.go @@ -2,8 +2,9 @@ package subtle import ( "encoding/hex" - "github.com/stretchr/testify/assert" "testing" + + "github.com/stretchr/testify/assert" ) func TestSubtle_EncryptWithoutIntegrity(t *testing.T) {