Skip to content

Commit

Permalink
feat(armor): Rework armor checksum handling (#284)
Browse files Browse the repository at this point in the history
* feat(armor): Armor checksum handling according to the crypto refresh

* docs(readme): Add info about v2 support

* refactor(armor): Improve checksum naming

* refactor(armor): Rename global checksum setting variable
  • Loading branch information
lubux authored Jun 14, 2024
1 parent a55a5f2 commit 5720941
Show file tree
Hide file tree
Showing 10 changed files with 246 additions and 53 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ 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).

## Unreleased
### Added
- API to armor data with the option to remove the checksum

### Changed
- All armor functions append a checksum per default for compatibility with certain libraries although the crypto-refresh advises not to.
- `Encryption` and `Sign` handle now append a checksum when armoring. If the produced OpenPGP packets are crypto-refresh packets, the checksum is not appended as mandated by the crypto-refresh.

## [3.0.0-alpha.2] 2024-04-12
### Added
- API to serialize KeyRings to binary data:
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ crypto library](https://github.com/ProtonMail/go-crypto/tree/version-2).
<!-- TOC depthFrom:2 -->

- [GopenPGP V3](#gopenpgp-v3)
- [GopenPGP V2 support](#gopenpgp-v2-support)
- [Download/Install](#downloadinstall)
- [Documentation](#documentation)
- [Examples](#examples)
Expand All @@ -21,6 +22,14 @@ crypto library](https://github.com/ProtonMail/go-crypto/tree/version-2).

<!-- /TOC -->

## GopenPGP V2 support

While GopenPGP V3 introduces a new API with significant enhancements, it is not backward compatible with GopenPGP V2.
Although we recommend upgrading to V3 for the latest features and improvements, we continue to support GopenPGP V2.
Our support includes ongoing bug fixes and minor feature updates to ensure stability and functionality for existing users.

You can access GopenPGP V2 on the [v2 branch of this repository](https://github.com/ProtonMail/gopenpgp/tree/v2).

## Download/Install

To use GopenPGP with [Go Modules](https://github.com/golang/go/wiki/Modules) just run
Expand Down
48 changes: 40 additions & 8 deletions armor/armor.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,14 @@ func ArmorKey(input []byte) (string, error) {
// ArmorWriterWithType returns a io.WriteCloser which, when written to, writes
// armored data to w with the given armorType.
func ArmorWriterWithType(w io.Writer, armorType string) (io.WriteCloser, error) {
return armor.EncodeWithChecksumOption(w, armorType, internal.ArmorHeaders, false)
return armor.EncodeWithChecksumOption(w, armorType, internal.ArmorHeaders, constants.ArmorChecksumEnabled)
}

// ArmorWriterWithTypeChecksum returns a io.WriteCloser which, when written to, writes
// armored data to w with the given armorType.
// The checksum determines if an armor checksum is written at the end.
func ArmorWriterWithTypeChecksum(w io.Writer, armorType string, checksum bool) (io.WriteCloser, error) {
return armor.EncodeWithChecksumOption(w, armorType, internal.ArmorHeaders, checksum)
}

// ArmorWriterWithTypeAndCustomHeaders returns a io.WriteCloser,
Expand All @@ -33,12 +40,18 @@ func ArmorWriterWithTypeAndCustomHeaders(w io.Writer, armorType, version, commen
if comment != "" {
headers["Comment"] = comment
}
return armor.EncodeWithChecksumOption(w, armorType, headers, false)
return armor.EncodeWithChecksumOption(w, armorType, headers, constants.ArmorChecksumEnabled)
}

// ArmorWithType armors input with the given armorType.
func ArmorWithType(input []byte, armorType string) (string, error) {
buffer, err := armorWithTypeAndHeaders(input, armorType, internal.ArmorHeaders)
return ArmorWithTypeChecksum(input, armorType, constants.ArmorChecksumEnabled)
}

// ArmorWithTypeChecksum armors input with the given armorType.
// The checksum option determines if an armor checksum is written at the end.
func ArmorWithTypeChecksum(input []byte, armorType string, checksum bool) (string, error) {
buffer, err := armorWithTypeAndHeaders(input, armorType, internal.ArmorHeaders, checksum)
if err != nil {
return "", err
}
Expand All @@ -47,7 +60,12 @@ func ArmorWithType(input []byte, armorType string) (string, error) {

// ArmorWithTypeBytes armors input with the given armorType.
func ArmorWithTypeBytes(input []byte, armorType string) ([]byte, error) {
buffer, err := armorWithTypeAndHeaders(input, armorType, internal.ArmorHeaders)
return ArmorWithTypeBytesChecksum(input, armorType, constants.ArmorChecksumEnabled)
}

// ArmorWithTypeBytesChecksum armors input with the given armorType and checksum option.
func ArmorWithTypeBytesChecksum(input []byte, armorType string, checksum bool) ([]byte, error) {
buffer, err := armorWithTypeAndHeaders(input, armorType, internal.ArmorHeaders, checksum)
if err != nil {
return nil, err
}
Expand All @@ -57,14 +75,20 @@ func ArmorWithTypeBytes(input []byte, armorType string) ([]byte, error) {
// ArmorWithTypeAndCustomHeaders armors input with the given armorType and
// headers.
func ArmorWithTypeAndCustomHeaders(input []byte, armorType, version, comment string) (string, error) {
return ArmorWithTypeAndCustomHeadersChecksum(input, armorType, version, comment, constants.ArmorChecksumEnabled)
}

// ArmorWithTypeAndCustomHeadersChecksum armors input with the given armorType and
// headers and checksum option.
func ArmorWithTypeAndCustomHeadersChecksum(input []byte, armorType, version, comment string, checksum bool) (string, error) {
headers := make(map[string]string)
if version != "" {
headers["Version"] = version
}
if comment != "" {
headers["Comment"] = comment
}
buffer, err := armorWithTypeAndHeaders(input, armorType, headers)
buffer, err := armorWithTypeAndHeaders(input, armorType, headers, checksum)
if err != nil {
return "", err
}
Expand All @@ -81,7 +105,7 @@ func ArmorWithTypeAndCustomHeadersBytes(input []byte, armorType, version, commen
if comment != "" {
headers["Comment"] = comment
}
buffer, err := armorWithTypeAndHeaders(input, armorType, headers)
buffer, err := armorWithTypeAndHeaders(input, armorType, headers, constants.ArmorChecksumEnabled)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -132,6 +156,14 @@ func ArmorPGPMessage(signature []byte) (string, error) {
return ArmorWithType(signature, constants.PGPMessageHeader)
}

func ArmorPGPMessageBytesChecksum(signature []byte, checksum bool) ([]byte, error) {
return ArmorWithTypeBytesChecksum(signature, constants.PGPMessageHeader, checksum)
}

func ArmorPGPMessageChecksum(signature []byte, checksum bool) (string, error) {
return ArmorWithTypeChecksum(signature, constants.PGPMessageHeader, checksum)
}

const armorPrefix = "-----BEGIN PGP"
const maxGarbageBytes = 128

Expand All @@ -150,10 +182,10 @@ func IsPGPArmored(in io.Reader) (io.Reader, bool) {
return outReader, false
}

func armorWithTypeAndHeaders(input []byte, armorType string, headers map[string]string) (*bytes.Buffer, error) {
func armorWithTypeAndHeaders(input []byte, armorType string, headers map[string]string, writeChecksum bool) (*bytes.Buffer, error) {
var b bytes.Buffer

w, err := armor.EncodeWithChecksumOption(&b, armorType, headers, false)
w, err := armor.EncodeWithChecksumOption(&b, armorType, headers, writeChecksum)

if err != nil {
return nil, errors.Wrap(err, "armor: unable to encode armoring")
Expand Down
21 changes: 14 additions & 7 deletions constants/armor.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,18 @@ package constants

// Constants for armored data.
const (
ArmorHeaderEnabled = false // can be enabled for debugging at compile time only
ArmorHeaderVersion = "GopenPGP " + Version
ArmorHeaderComment = "https://gopenpgp.org"
PGPMessageHeader = "PGP MESSAGE"
PGPSignatureHeader = "PGP SIGNATURE"
PublicKeyHeader = "PGP PUBLIC KEY BLOCK"
PrivateKeyHeader = "PGP PRIVATE KEY BLOCK"
// ArmorChecksumEnabled defines the default behavior for adding an armor checksum
// to an armored message.
//
// If set to true, an armor checksum is added to the message.
//
// If set to false, no armor checksum is added.
ArmorChecksumEnabled = true
ArmorHeaderEnabled = false // can be enabled for debugging at compile time only
ArmorHeaderVersion = "GopenPGP " + Version
ArmorHeaderComment = "https://gopenpgp.org"
PGPMessageHeader = "PGP MESSAGE"
PGPSignatureHeader = "PGP SIGNATURE"
PublicKeyHeader = "PGP PUBLIC KEY BLOCK"
PrivateKeyHeader = "PGP PRIVATE KEY BLOCK"
)
30 changes: 30 additions & 0 deletions crypto/encrypt_decrypt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"io"
"os"
"reflect"
"regexp"
"strings"
"testing"

Expand Down Expand Up @@ -1006,6 +1007,35 @@ func TestEncryptDecryptPlaintextDetachedArmor(t *testing.T) {
}
}

func TestEncryptArmor(t *testing.T) {
for _, material := range testMaterialForProfiles {
t.Run(material.profileName, func(t *testing.T) {
isV6 := material.keyRingTestPublic.GetKeys()[0].isV6()
encHandle, _ := material.pgp.Encryption().
Recipients(material.keyRingTestPublic).
SigningKeys(material.keyRingTestPrivate).
New()
pgpMsg, err := encHandle.Encrypt([]byte(testMessageString))
if err != nil {
t.Fatal("Expected no error in encryption, got:", err)
}
armoredData, err := pgpMsg.Armor()
if err != nil {
t.Fatal("Armoring failed, got:", err)
}
hasChecksum := containsChecksum(armoredData)
if isV6 && hasChecksum {
t.Fatalf("V6 messages should not have a checksum")
}
})
}
}

func containsChecksum(armored string) bool {
re := regexp.MustCompile(`=([A-Za-z0-9+/]{4})\s*-----END PGP MESSAGE-----`)
return re.MatchString(armored)
}

func testEncryptDecrypt(
t *testing.T,
messageBytes []byte,
Expand Down
104 changes: 79 additions & 25 deletions crypto/encryption_handle.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ func (eh *encryptionHandle) Encrypt(message []byte) (*PGPMessage, error) {
if err != nil {
return nil, err
}
return pgpMessageBuffer.PGPMessageWithDetached(eh.PlainDetachedSignature), nil
checksum := eh.armorChecksumRequired()
return pgpMessageBuffer.PGPMessageWithOptions(eh.PlainDetachedSignature, !checksum), nil
}

// EncryptSessionKey encrypts a session key with the encryption handle.
Expand Down Expand Up @@ -150,6 +151,44 @@ func (eh *encryptionHandle) validate() error {
return nil
}

// armorChecksumRequired determines if an armor checksum should be appended or not.
// The OpenPGP Crypto-Refresh mandates that no checksum should be appended with the new packets.
func (eh *encryptionHandle) armorChecksumRequired() bool {
if !constants.ArmorChecksumEnabled {
// If the default behavior is no checksum, we can ignore
// the logic for the crypto refresh check.
return false
}
encryptionConfig := eh.profile.EncryptionConfig()
if encryptionConfig.AEADConfig == nil {
return true
}
checkTime := eh.clock()
if eh.Recipients != nil {
for _, recipient := range eh.Recipients.entities {
primarySelfSignature, err := recipient.PrimarySelfSignature(checkTime)
if err != nil {
return true
}
if !primarySelfSignature.SEIPDv2 {
return true
}
}
}
if eh.HiddenRecipients != nil {
for _, recipient := range eh.HiddenRecipients.entities {
primarySelfSignature, err := recipient.PrimarySelfSignature(checkTime)
if err != nil {
return true
}
if !primarySelfSignature.SEIPDv2 {
return true
}
}
}
return false
}

type armoredWriteCloser struct {
armorWriter WriteCloser
messageWriter WriteCloser
Expand Down Expand Up @@ -185,11 +224,47 @@ func (eh *encryptionHandle) ClearPrivateParams() {
}
}

func (eh *encryptionHandle) handleArmor(keys, data, detachedSignature Writer) (
dataOut Writer,
detachedSignatureOut Writer,
armorWriter WriteCloser,
armorSigWriter WriteCloser,
err error,
) {
writeChecksum := eh.armorChecksumRequired()
detachedSignatureOut = detachedSignature
// Wrap armored writer
if eh.ArmorHeaders == nil {
eh.ArmorHeaders = internal.ArmorHeaders
}
armorWriter, err = armor.EncodeWithChecksumOption(data, constants.PGPMessageHeader, eh.ArmorHeaders, writeChecksum)
dataOut = armorWriter
if err != nil {
return nil, nil, nil, nil, err
}
if eh.DetachedSignature {
armorSigWriter, err = armor.EncodeWithChecksumOption(detachedSignature, constants.PGPMessageHeader, eh.ArmorHeaders, writeChecksum)
detachedSignatureOut = armorSigWriter
if err != nil {
return nil, nil, nil, nil, err
}
} else if eh.PlainDetachedSignature {
armorSigWriter, err = armor.EncodeWithChecksumOption(detachedSignature, constants.PGPSignatureHeader, eh.ArmorHeaders, writeChecksum)
detachedSignatureOut = armorSigWriter
if err != nil {
return nil, nil, nil, nil, err
}
}
if keys != nil {
return nil, nil, nil, nil, errors.New("gopenpgp: armor is not allowed if key packets are written separately")
}
return dataOut, detachedSignatureOut, armorWriter, armorSigWriter, nil
}

func (eh *encryptionHandle) encryptingWriters(keys, data, detachedSignature Writer, meta *LiteralMetadata, armorOutput bool) (messageWriter WriteCloser, err error) {
var armorWriter WriteCloser
var armorSigWriter WriteCloser
err = eh.validate()
if err != nil {
if err = eh.validate(); err != nil {
return nil, err
}

Expand All @@ -199,31 +274,10 @@ func (eh *encryptionHandle) encryptingWriters(keys, data, detachedSignature Writ
}

if armorOutput {
// Wrap armored writer
if eh.ArmorHeaders == nil {
eh.ArmorHeaders = internal.ArmorHeaders
}
armorWriter, err = armor.EncodeWithChecksumOption(data, constants.PGPMessageHeader, eh.ArmorHeaders, false)
data = armorWriter
data, detachedSignature, armorWriter, armorSigWriter, err = eh.handleArmor(keys, data, detachedSignature)
if err != nil {
return nil, err
}
if eh.DetachedSignature {
armorSigWriter, err = armor.EncodeWithChecksumOption(detachedSignature, constants.PGPMessageHeader, eh.ArmorHeaders, false)
detachedSignature = armorSigWriter
if err != nil {
return nil, err
}
} else if eh.PlainDetachedSignature {
armorSigWriter, err = armor.EncodeWithChecksumOption(detachedSignature, constants.PGPSignatureHeader, eh.ArmorHeaders, false)
detachedSignature = armorSigWriter
if err != nil {
return nil, err
}
}
if keys != nil {
return nil, errors.New("gopenpgp: armor is not allowed if key packets are written separately")
}
}
if keys == nil {
// No writer for key packets provided,
Expand Down
Loading

0 comments on commit 5720941

Please sign in to comment.