github.com/readium/readium-lcp-server@v0.0.0-20240101192032-6e95190e99f1/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 // espace 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 }