Browse Source

openpgp: Implement compressed data packets & add support for compressing data during symmetric encryption.

This patch implements the facilities needed to compress data before
encryption as allowed (and recommended) by RFC 4880. The new
functionality is then used to add support for compressing data during
symmetric encryption (openpgp.SymmetricallyEncrypt()).

For now, compression defaults to off. Also, only the ZIP and ZLIB
compression schemes are supported by this patch.

Resulting output tested/verified using GPG.

https://gist.github.com/marete/6189760 is a small program that can be
used to test that the output of various compression/encryption settings
can be read by GPG or other RFC 4880 programs.

Upon review, I will follow this patch with 2 others: a) Add support for 	compression during public key encryption (openpgp.Encrypt()) b) Enable
compression by default (subject to the restrictions of the "Compression 	Preferences" section in RFC 4880).

R=golang-dev, agl
CC=golang-dev
https://golang.org/cl/12685044
Brian Gitonga Marete 12 years ago
parent
commit
690e22b80a
4 changed files with 120 additions and 1 deletions
  1. 82 0
      openpgp/packet/compressed.go
  2. 13 0
      openpgp/packet/config.go
  3. 11 0
      openpgp/packet/packet.go
  4. 14 1
      openpgp/write.go

+ 82 - 0
openpgp/packet/compressed.go

@@ -19,6 +19,26 @@ type Compressed struct {
 	Body io.Reader
 }
 
+const (
+	NoCompression      = flate.NoCompression
+	BestSpeed          = flate.BestSpeed
+	BestCompression    = flate.BestCompression
+	DefaultCompression = flate.DefaultCompression
+)
+
+// CompressionConfig contains compressor configuration settings.
+type CompressionConfig struct {
+	// Level is the compression level to use. It must be set to
+	// between -1 and 9, with -1 causing the compressor to use the
+	// default compression level, 0 causing the compressor to use
+	// no compression and 1 to 9 representing increasing (better,
+	// slower) compression levels. If Level is less than -1 or
+	// more then 9, a non-nil error will be returned during
+	// encryption. See the constants above for convenient common
+	// settings for Level.
+	Level int
+}
+
 func (c *Compressed) parse(r io.Reader) error {
 	var buf [1]byte
 	_, err := readFull(r, buf[:])
@@ -39,3 +59,65 @@ func (c *Compressed) parse(r io.Reader) error {
 
 	return err
 }
+
+// compressedWriterCloser represents the serialized compression stream
+// header and the compressor. Its Close() method ensures that both the
+// compressor and serialized stream header are closed. Its Write()
+// method writes to the compressor.
+type compressedWriteCloser struct {
+	sh io.Closer      // Stream Header
+	c  io.WriteCloser // Compressor
+}
+
+func (cwc compressedWriteCloser) Write(p []byte) (int, error) {
+	return cwc.c.Write(p)
+}
+
+func (cwc compressedWriteCloser) Close() (err error) {
+	err = cwc.c.Close()
+	if err != nil {
+		return err
+	}
+
+	return cwc.sh.Close()
+}
+
+// SerializeCompressed serializes a compressed data packet to w and
+// returns a WriteCloser to which the literal data packets themselves
+// can be written and which MUST be closed on completion. If cc is
+// nil, sensible defaults will be used to configure the compression
+// algorithm.
+func SerializeCompressed(w io.WriteCloser, algo CompressionAlgo, cc *CompressionConfig) (literaldata io.WriteCloser, err error) {
+	compressed, err := serializeStreamHeader(w, packetTypeCompressed)
+	if err != nil {
+		return
+	}
+
+	_, err = compressed.Write([]byte{uint8(algo)})
+	if err != nil {
+		return
+	}
+
+	level := DefaultCompression
+	if cc != nil {
+		level = cc.Level
+	}
+
+	var compressor io.WriteCloser
+	switch algo {
+	case CompressionZIP:
+		compressor, err = flate.NewWriter(compressed, level)
+	case CompressionZLIB:
+		compressor, err = zlib.NewWriterLevel(compressed, level)
+	default:
+		s := strconv.Itoa(int(algo))
+		err = errors.UnsupportedError("Unsupported compression algorithm: " + s)
+	}
+	if err != nil {
+		return
+	}
+
+	literaldata = compressedWriteCloser{compressed, compressor}
+
+	return
+}

+ 13 - 0
openpgp/packet/config.go

@@ -26,6 +26,12 @@ type Config struct {
 	// Time returns the current time as the number of seconds since the
 	// epoch. If Time is nil, time.Now is used.
 	Time func() time.Time
+	// DefaultCompressionAlgo is the compression algorithm to be
+	// applied to the plaintext before encryption. If zero, no
+	// compression is done.
+	DefaultCompressionAlgo CompressionAlgo
+	// CompressionConfig configures the compression settings.
+	CompressionConfig *CompressionConfig
 }
 
 func (c *Config) Random() io.Reader {
@@ -55,3 +61,10 @@ func (c *Config) Now() time.Time {
 	}
 	return c.Time()
 }
+
+func (c *Config) Compression() CompressionAlgo {
+	if c == nil {
+		return CompressionNone
+	}
+	return c.DefaultCompressionAlgo
+}

+ 11 - 0
openpgp/packet/packet.go

@@ -488,3 +488,14 @@ func writeMPI(w io.Writer, bitLength uint16, mpiBytes []byte) (err error) {
 func writeBig(w io.Writer, i *big.Int) error {
 	return writeMPI(w, uint16(i.BitLen()), i.Bytes())
 }
+
+// CompressionAlgo Represents the different compression algorithms
+// supported by OpenPGP (except for BZIP2, which is not currently
+// supported). See Section 9.3 of RFC 4880.
+type CompressionAlgo uint8
+
+const (
+	CompressionNone CompressionAlgo = 0
+	CompressionZIP  CompressionAlgo = 1
+	CompressionZLIB CompressionAlgo = 2
+)

+ 14 - 1
openpgp/write.go

@@ -118,11 +118,24 @@ func SymmetricallyEncrypt(ciphertext io.Writer, passphrase []byte, hints *FileHi
 	if err != nil {
 		return
 	}
+
+	literaldata := w
+	if algo := config.Compression(); algo != packet.CompressionNone {
+		var compConfig *packet.CompressionConfig
+		if config != nil {
+			compConfig = config.CompressionConfig
+		}
+		literaldata, err = packet.SerializeCompressed(w, algo, compConfig)
+		if err != nil {
+			return
+		}
+	}
+
 	var epochSeconds uint32
 	if !hints.ModTime.IsZero() {
 		epochSeconds = uint32(hints.ModTime.Unix())
 	}
-	return packet.SerializeLiteral(w, hints.IsBinary, hints.FileName, epochSeconds)
+	return packet.SerializeLiteral(literaldata, hints.IsBinary, hints.FileName, epochSeconds)
 }
 
 // intersectPreferences mutates and returns a prefix of a that contains only