github.com/readium/readium-lcp-server@v0.0.0-20240509124024-799e77a0bbd6/pack/pack.go (about)

     1  // Copyright 2020 Readium Foundation. All rights reserved.
     2  // Use of this source code is governed by a BSD-style license
     3  // that can be found in the LICENSE file exposed on Github (readium) in the project repository.
     4  
     5  package pack
     6  
     7  import (
     8  	"bytes"
     9  	"compress/flate"
    10  	"encoding/base64"
    11  	"io"
    12  	"log"
    13  	"strings"
    14  
    15  	"github.com/readium/readium-lcp-server/crypto"
    16  	"github.com/readium/readium-lcp-server/epub"
    17  	"github.com/readium/readium-lcp-server/xmlenc"
    18  )
    19  
    20  // PackageReader is an interface
    21  type PackageReader interface {
    22  	Resources() []Resource
    23  	NewWriter(io.Writer) (PackageWriter, error)
    24  }
    25  
    26  // PackageWriter is an interface
    27  type PackageWriter interface {
    28  	NewFile(path string, contentType string, storageMethod uint16) (io.WriteCloser, error)
    29  	MarkAsEncrypted(path string, originalSize int64, algorithm string)
    30  	Close() error
    31  }
    32  
    33  // Resource is an interface
    34  type Resource interface {
    35  	Path() string
    36  	Size() int64
    37  	ContentType() string
    38  	CompressBeforeEncryption() bool
    39  	CanBeEncrypted() bool
    40  	Encrypted() bool
    41  	CopyTo(PackageWriter) error
    42  	Open() (io.ReadCloser, error)
    43  }
    44  
    45  func getOrSetContentKey(encrypter crypto.Encrypter, contentKey string) (key crypto.ContentKey, err error) {
    46  	if contentKey != "" {
    47  		key, err = base64.StdEncoding.DecodeString(contentKey)
    48  		if err != nil {
    49  			log.Println("Error unpacking given content key")
    50  			return nil, err
    51  		}
    52  	} else {
    53  		key, err = encrypter.GenerateKey()
    54  		if err != nil {
    55  			log.Println("Error generating an encryption key")
    56  			return nil, err
    57  		}
    58  	}
    59  	return key, nil
    60  }
    61  
    62  // Process copies resources from the source to the destination package, after encryption if needed.
    63  func Process(encrypter crypto.Encrypter, contentKey string, reader PackageReader, writer PackageWriter) (key crypto.ContentKey, err error) {
    64  
    65  	if key, err = getOrSetContentKey(encrypter, contentKey); err != nil {
    66  		return
    67  	}
    68  	// create a compressing tool
    69  	var buf bytes.Buffer
    70  	compressor, err := flate.NewWriter(&buf, flate.BestCompression)
    71  	if err != nil {
    72  		return
    73  	}
    74  
    75  	// loop through the resources of the source package, encrypt them if needed, copy them into the dest package
    76  	for _, resource := range reader.Resources() {
    77  		if !resource.Encrypted() && resource.CanBeEncrypted() {
    78  			if resource.(*rwpResource).file == nil {
    79  				log.Println("Error encrypting a file: Nil file name")
    80  				return
    81  			}
    82  			err = encryptRPFResource(compressor, encrypter, key, resource, writer)
    83  			if err != nil {
    84  				log.Println("Error encrypting ", resource.Path(), ": ", err.Error())
    85  				return
    86  			}
    87  		} else {
    88  			err = resource.CopyTo(writer)
    89  			if err != nil {
    90  				log.Println("Error copying the file")
    91  				return
    92  			}
    93  		}
    94  	}
    95  
    96  	// close the compressor
    97  	if err = compressor.Close(); err != nil {
    98  		return
    99  	}
   100  
   101  	return
   102  }
   103  
   104  // Do encrypts when necessary the resources of an EPUB package
   105  // It is called for EPUB files only
   106  // FIXME: try to merge Process() and Do()
   107  func Do(encrypter crypto.Encrypter, contentKey string, ep epub.Epub, w io.Writer) (enc *xmlenc.Manifest, key crypto.ContentKey, err error) {
   108  
   109  	// generate an encryption key
   110  	if key, err = getOrSetContentKey(encrypter, contentKey); err != nil {
   111  		return
   112  	}
   113  
   114  	// initialise the target publication
   115  	ew := epub.NewWriter(w)
   116  	ew.WriteHeader()
   117  	if ep.Encryption == nil {
   118  		ep.Encryption = &xmlenc.Manifest{}
   119  	}
   120  
   121  	// create a compressor
   122  	var buf bytes.Buffer
   123  	compressor, err := flate.NewWriter(&buf, flate.BestCompression)
   124  	if err != nil {
   125  		return
   126  	}
   127  
   128  	for _, res := range ep.Resource {
   129  		if _, alreadyEncrypted := ep.Encryption.DataForFile(res.Path); !alreadyEncrypted && canEncrypt(res, ep) {
   130  			compress := mustCompressBeforeEncryption(*res, ep)
   131  			// encrypt the resource after optionally compressing it
   132  			err = encryptEPUBResource(compressor, compress, encrypter, key, ep.Encryption, res, ew)
   133  			if err != nil {
   134  				log.Println("Error encrypting ", res.Path, ": ", err.Error())
   135  				return
   136  			}
   137  		} else {
   138  			// copy the resource as-is to the target publication
   139  			err = ew.Copy(res)
   140  			if err != nil {
   141  				log.Println("Error copying the file")
   142  				return
   143  			}
   144  		}
   145  	}
   146  
   147  	// save the encryption manifest
   148  	ew.WriteEncryption(ep.Encryption)
   149  
   150  	// close the compressor
   151  	if err = compressor.Close(); err != nil {
   152  		return
   153  	}
   154  
   155  	return ep.Encryption, key, ew.Close()
   156  }
   157  
   158  // mustCompressBeforeEncryption checks is a resource must be compressed before encryption.
   159  // We don't want to compress files if that might cause streaming (byte range requests) issues.
   160  // The test is applied on the resource media-type; image, video, audio, pdf are stored without compression.
   161  func mustCompressBeforeEncryption(file epub.Resource, ep epub.Epub) bool {
   162  
   163  	mimetype := file.ContentType
   164  
   165  	if mimetype == "" {
   166  		return true
   167  	}
   168  
   169  	return !strings.HasPrefix(mimetype, "image") && !strings.HasPrefix(mimetype, "video") && !strings.HasPrefix(mimetype, "audio") && !(mimetype == "application/pdf")
   170  }
   171  
   172  // NoCompression means Store
   173  const (
   174  	NoCompression = 0
   175  	Deflate       = 8
   176  )
   177  
   178  // canEncrypt checks if a resource should be encrypted
   179  func canEncrypt(file *epub.Resource, ep epub.Epub) bool {
   180  	return ep.CanEncrypt(file.Path)
   181  }
   182  
   183  // encryptRPFResource encrypts a resource in a Readium Package
   184  func encryptRPFResource(compressor *flate.Writer, encrypter crypto.Encrypter, key crypto.ContentKey, resource Resource, packageWriter PackageWriter) error {
   185  
   186  	// add the file to the package writer
   187  	// note: the file is stored as-is because compression, when applied, is applied *before* encryption
   188  	file, err := packageWriter.NewFile(resource.Path(), resource.ContentType(), uint16(NoCompression))
   189  	if err != nil {
   190  		return err
   191  	}
   192  	resourceReader, err := resource.Open()
   193  	if err != nil {
   194  		return err
   195  	}
   196  	var reader io.Reader = resourceReader
   197  
   198  	// FIXME: CompressBeforeEncryption() is currently always set to false
   199  	if resource.CompressBeforeEncryption() {
   200  
   201  		// use a new buffer as target of the compressor
   202  		var buf bytes.Buffer
   203  		compressor.Reset(&buf)
   204  		io.Copy(compressor, resourceReader)
   205  		if err := compressor.Close(); err != nil {
   206  			return err
   207  		}
   208  		// use the buffer as source of the encryption
   209  		reader = &buf
   210  	}
   211  
   212  	err = encrypter.Encrypt(key, reader, file)
   213  
   214  	resourceReader.Close()
   215  	file.Close()
   216  
   217  	packageWriter.MarkAsEncrypted(resource.Path(), resource.Size(), encrypter.Signature())
   218  
   219  	return err
   220  }
   221  
   222  // encryptEPUBResource encrypts a file in an EPUB package
   223  func encryptEPUBResource(compressor *flate.Writer, compress bool, encrypter crypto.Encrypter, key []byte, m *xmlenc.Manifest, file *epub.Resource, w *epub.Writer) error {
   224  
   225  	// set encryption properties for the resource
   226  	data := xmlenc.Data{}
   227  	data.Method.Algorithm = xmlenc.URI(encrypter.Signature())
   228  	data.KeyInfo = &xmlenc.KeyInfo{}
   229  	data.KeyInfo.RetrievalMethod.URI = "license.lcpl#/encryption/content_key"
   230  	data.KeyInfo.RetrievalMethod.Type = "http://readium.org/2014/01/lcp#EncryptedContentKey"
   231  
   232  	// escape the path before using it as a uri
   233  	data.CipherData.CipherReference.URI = xmlenc.URI(xmlenc.ResourcePathEscape(file.Path))
   234  
   235  	// declare to the reading software that the content is compressed before encryption
   236  	method := NoCompression
   237  	if compress {
   238  		method = Deflate
   239  	}
   240  	data.Properties = &xmlenc.EncryptionProperties{
   241  		Properties: []xmlenc.EncryptionProperty{
   242  			{Compression: xmlenc.Compression{Method: method, OriginalLength: file.OriginalSize}},
   243  		},
   244  	}
   245  
   246  	m.Data = append(m.Data, data)
   247  
   248  	// by default, the source file is the source of the encryption
   249  	input := file.Contents
   250  
   251  	// if the content has to be compressed before encryption
   252  	if compress {
   253  		// use a new buffer as target of the compressor
   254  		var buf bytes.Buffer
   255  		compressor.Reset(&buf)
   256  		io.Copy(compressor, file.Contents)
   257  		if err := compressor.Close(); err != nil {
   258  			return err
   259  		}
   260  		//file.ContentsSize = uint64(buf.Len())
   261  		// use the buffer as source of the encryption
   262  		input = &buf
   263  	}
   264  
   265  	// note: the file is stored as-is in the zip because compression, when applied, is applied before encryption
   266  	// and therefore *before* storage.
   267  	file.StorageMethod = NoCompression
   268  
   269  	fw, err := w.AddResource(file.Path, NoCompression)
   270  	if err != nil {
   271  		return err
   272  	}
   273  	// encrypt the buffer and store the resulting resource in the target publication
   274  	return encrypter.Encrypt(key, input, fw)
   275  }
   276  
   277  // FindFile finds a file in an EPUB object
   278  func FindFile(name string, ep epub.Epub) (*epub.Resource, bool) {
   279  
   280  	for _, res := range ep.Resource {
   281  		if res.Path == name {
   282  			return res, true
   283  		}
   284  	}
   285  
   286  	return nil, false
   287  }