From b6941e5a5bfdcdeff3f35983ba40dd8bf224875d Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 29 May 2022 19:37:10 +0800 Subject: [PATCH] This initialized support for encryption workbook by password, ref #199 - Remove exported variable `ErrEncrypt` --- crypt.go | 907 ++++++++++++++++++++++++++++++++++++++++++-------- crypt_test.go | 19 +- errors.go | 2 - excelize.go | 4 - file.go | 3 + 5 files changed, 780 insertions(+), 155 deletions(-) diff --git a/crypt.go b/crypt.go index da9feb4a653..65d5dae2b92 100644 --- a/crypt.go +++ b/crypt.go @@ -15,7 +15,6 @@ import ( "bytes" "crypto/aes" "crypto/cipher" - "crypto/hmac" "crypto/md5" "crypto/rand" "crypto/sha1" @@ -25,6 +24,7 @@ import ( "encoding/binary" "encoding/xml" "hash" + "math" "reflect" "strings" @@ -36,17 +36,11 @@ import ( var ( blockKey = []byte{0x14, 0x6e, 0x0b, 0xe7, 0xab, 0xac, 0xd0, 0xd6} // Block keys used for encryption - blockKeyHmacKey = []byte{0x5f, 0xb2, 0xad, 0x01, 0x0c, 0xb9, 0xe1, 0xf6} - blockKeyHmacValue = []byte{0xa0, 0x67, 0x7f, 0x02, 0xb2, 0x2c, 0x84, 0x33} - blockKeyVerifierHashInput = []byte{0xfe, 0xa7, 0xd2, 0x76, 0x3b, 0x4b, 0x9e, 0x79} - blockKeyVerifierHashValue = []byte{0xd7, 0xaa, 0x0f, 0x6d, 0x30, 0x61, 0x34, 0x4e} - packageOffset = 8 // First 8 bytes are the size of the stream - packageEncryptionChunkSize = 4096 + oleIdentifier = []byte{0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1} iterCount = 50000 + packageEncryptionChunkSize = 4096 + packageOffset = 8 // First 8 bytes are the size of the stream sheetProtectionSpinCount = 1e5 - oleIdentifier = []byte{ - 0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1, - } ) // Encryption specifies the encryption structure, streams, and storages are @@ -128,6 +122,14 @@ type StandardEncryptionVerifier struct { EncryptedVerifierHash []byte } +// encryptionInfo structure is used for standard encryption with SHA1 +// cryptographic algorithm. +type encryption struct { + BlockSize, SaltSize int + EncryptedKeyValue, EncryptedVerifierHashInput, EncryptedVerifierHashValue, SaltValue []byte + KeyBits uint32 +} + // Decrypt API decrypts the CFB file format with ECMA-376 agile encryption and // standard encryption. Support cryptographic algorithm: MD4, MD5, RIPEMD-160, // SHA1, SHA256, SHA384 and SHA512 currently. @@ -154,125 +156,27 @@ func Decrypt(raw []byte, opt *Options) (packageBuf []byte, err error) { // Encrypt API encrypt data with the password. func Encrypt(raw []byte, opt *Options) (packageBuf []byte, err error) { - // Generate a random key to use to encrypt the document. Excel uses 32 bytes. We'll use the password to encrypt this key. - packageKey, _ := randomBytes(32) - keyDataSaltValue, _ := randomBytes(16) - keyEncryptors, _ := randomBytes(16) - encryptionInfo := Encryption{ - KeyData: KeyData{ - BlockSize: 16, - KeyBits: len(packageKey) * 8, - HashSize: 64, - CipherAlgorithm: "AES", - CipherChaining: "ChainingModeCBC", - HashAlgorithm: "SHA512", - SaltValue: base64.StdEncoding.EncodeToString(keyDataSaltValue), - }, - KeyEncryptors: KeyEncryptors{ - KeyEncryptor: []KeyEncryptor{{ - EncryptedKey: EncryptedKey{ - SpinCount: 100000, KeyData: KeyData{ - CipherAlgorithm: "AES", - CipherChaining: "ChainingModeCBC", - HashAlgorithm: "SHA512", - HashSize: 64, - BlockSize: 16, - KeyBits: 256, - SaltValue: base64.StdEncoding.EncodeToString(keyEncryptors), - }, - }, - }}, - }, - } - - // Package Encryption - - // Encrypt package using the package key. - encryptedPackage, err := cryptPackage(true, packageKey, raw, encryptionInfo) - if err != nil { - return - } - - // Data Integrity - - // Create the data integrity fields used by clients for integrity checks. - // Generate a random array of bytes to use in HMAC. The docs say to use the same length as the key salt, but Excel seems to use 64. - hmacKey, _ := randomBytes(64) - if err != nil { - return - } - // Create an initialization vector using the package encryption info and the appropriate block key. - hmacKeyIV, err := createIV(blockKeyHmacKey, encryptionInfo) - if err != nil { - return + encryptor := encryption{ + EncryptedVerifierHashInput: make([]byte, 16), + EncryptedVerifierHashValue: make([]byte, 32), + SaltValue: make([]byte, 16), + BlockSize: 16, + KeyBits: 128, + SaltSize: 16, } - // Use the package key and the IV to encrypt the HMAC key. - encryptedHmacKey, _ := crypt(true, encryptionInfo.KeyData.CipherAlgorithm, encryptionInfo.KeyData.CipherChaining, packageKey, hmacKeyIV, hmacKey) - // Create the HMAC. - h := hmac.New(sha512.New, append(hmacKey, encryptedPackage...)) - for _, buf := range [][]byte{hmacKey, encryptedPackage} { - _, _ = h.Write(buf) - } - hmacValue := h.Sum(nil) - // Generate an initialization vector for encrypting the resulting HMAC value. - hmacValueIV, err := createIV(blockKeyHmacValue, encryptionInfo) - if err != nil { - return - } - // Encrypt the value. - encryptedHmacValue, _ := crypt(true, encryptionInfo.KeyData.CipherAlgorithm, encryptionInfo.KeyData.CipherChaining, packageKey, hmacValueIV, hmacValue) - // Put the encrypted key and value on the encryption info. - encryptionInfo.DataIntegrity.EncryptedHmacKey = base64.StdEncoding.EncodeToString(encryptedHmacKey) - encryptionInfo.DataIntegrity.EncryptedHmacValue = base64.StdEncoding.EncodeToString(encryptedHmacValue) - // Key Encryption - - // Convert the password to an encryption key. - key, err := convertPasswdToKey(opt.Password, blockKey, encryptionInfo) - if err != nil { - return - } - // Encrypt the package key with the encryption key. - encryptedKeyValue, _ := crypt(true, encryptionInfo.KeyEncryptors.KeyEncryptor[0].EncryptedKey.CipherAlgorithm, encryptionInfo.KeyEncryptors.KeyEncryptor[0].EncryptedKey.CipherChaining, key, keyEncryptors, packageKey) - encryptionInfo.KeyEncryptors.KeyEncryptor[0].EncryptedKey.EncryptedKeyValue = base64.StdEncoding.EncodeToString(encryptedKeyValue) - - // Verifier hash - - // Create a random byte array for hashing. - verifierHashInput, _ := randomBytes(16) - // Create an encryption key from the password for the input. - verifierHashInputKey, err := convertPasswdToKey(opt.Password, blockKeyVerifierHashInput, encryptionInfo) - if err != nil { - return - } - // Use the key to encrypt the verifier input. - encryptedVerifierHashInput, err := crypt(true, encryptionInfo.KeyData.CipherAlgorithm, encryptionInfo.KeyData.CipherChaining, verifierHashInputKey, keyEncryptors, verifierHashInput) - if err != nil { - return - } - encryptionInfo.KeyEncryptors.KeyEncryptor[0].EncryptedKey.EncryptedVerifierHashInput = base64.StdEncoding.EncodeToString(encryptedVerifierHashInput) - // Create a hash of the input. - verifierHashValue := hashing(encryptionInfo.KeyData.HashAlgorithm, verifierHashInput) - // Create an encryption key from the password for the hash. - verifierHashValueKey, err := convertPasswdToKey(opt.Password, blockKeyVerifierHashValue, encryptionInfo) - if err != nil { - return - } - // Use the key to encrypt the hash value. - encryptedVerifierHashValue, err := crypt(true, encryptionInfo.KeyData.CipherAlgorithm, encryptionInfo.KeyData.CipherChaining, verifierHashValueKey, keyEncryptors, verifierHashValue) + encryptionInfoBuffer, err := encryptor.standardKeyEncryption(opt.Password) if err != nil { - return - } - encryptionInfo.KeyEncryptors.KeyEncryptor[0].EncryptedKey.EncryptedVerifierHashValue = base64.StdEncoding.EncodeToString(encryptedVerifierHashValue) - // Marshal the encryption info buffer. - encryptionInfoBuffer, err := xml.Marshal(encryptionInfo) - if err != nil { - return + return nil, err } - // TODO: Create a new CFB. - _, _ = encryptedPackage, encryptionInfoBuffer - err = ErrEncrypt - return + // Package Encryption + encryptedPackage := make([]byte, 8) + binary.LittleEndian.PutUint64(encryptedPackage, uint64(len(raw))) + encryptedPackage = append(encryptedPackage, encryptor.encrypt(raw)...) + // Create a new CFB + compoundFile := cfb{} + packageBuf = compoundFile.Writer(encryptionInfoBuffer, encryptedPackage) + return packageBuf, nil } // extractPart extract data from storage by specified part name. @@ -419,6 +323,68 @@ func standardXORBytes(a, b []byte) []byte { return buf } +// encrypt provides a function to encrypt given value with AES cryptographic +// algorithm. +func (e *encryption) encrypt(input []byte) []byte { + inputBytes := len(input) + if pad := inputBytes % e.BlockSize; pad != 0 { + inputBytes += e.BlockSize - pad + } + var output, chunk []byte + encryptedChunk := make([]byte, e.BlockSize) + for i := 0; i < inputBytes; i += e.BlockSize { + if i+e.BlockSize <= len(input) { + chunk = input[i : i+e.BlockSize] + } else { + chunk = input[i:] + } + chunk = append(chunk, make([]byte, e.BlockSize-len(chunk))...) + c, _ := aes.NewCipher(e.EncryptedKeyValue) + c.Encrypt(encryptedChunk, chunk) + output = append(output, encryptedChunk...) + } + return output +} + +// standardKeyEncryption encrypt convert the password to an encryption key. +func (e *encryption) standardKeyEncryption(password string) ([]byte, error) { + if len(password) == 0 || len(password) > MaxFieldLength { + return nil, ErrPasswordLengthInvalid + } + var storage cfb + storage.writeUint16(0x0003) + storage.writeUint16(0x0002) + storage.writeUint32(0x24) + storage.writeUint32(0xA4) + storage.writeUint32(0x24) + storage.writeUint32(0x00) + storage.writeUint32(0x660E) + storage.writeUint32(0x8004) + storage.writeUint32(0x80) + storage.writeUint32(0x18) + storage.writeUint64(0x00) + providerName := "Microsoft Enhanced RSA and AES Cryptographic Provider (Prototype)" + storage.writeStrings(providerName) + storage.writeUint16(0x00) + storage.writeUint32(0x10) + keyDataSaltValue, _ := randomBytes(16) + verifierHashInput, _ := randomBytes(16) + e.SaltValue = keyDataSaltValue + e.EncryptedKeyValue, _ = standardConvertPasswdToKey( + StandardEncryptionHeader{KeySize: e.KeyBits}, + StandardEncryptionVerifier{Salt: e.SaltValue}, + &Options{Password: password}) + verifierHashInputKey := hashing("sha1", verifierHashInput) + e.EncryptedVerifierHashInput = e.encrypt(verifierHashInput) + e.EncryptedVerifierHashValue = e.encrypt(verifierHashInputKey) + storage.writeBytes(e.SaltValue) + storage.writeBytes(e.EncryptedVerifierHashInput) + storage.writeUint32(0x14) + storage.writeBytes(e.EncryptedVerifierHashValue) + storage.position = 0 + return storage.stream, nil +} + // ECMA-376 Agile Encryption // agileDecrypt decrypt the CFB file format with ECMA-376 agile encryption. @@ -444,9 +410,9 @@ func agileDecrypt(encryptionInfoBuf, encryptedPackageBuf []byte, opt *Options) ( if err != nil { return } - packageKey, _ := crypt(false, encryptedKey.CipherAlgorithm, encryptedKey.CipherChaining, key, saltValue, encryptedKeyValue) + packageKey, _ := decrypt(key, saltValue, encryptedKeyValue) // Use the package key to decrypt the package. - return cryptPackage(false, packageKey, encryptedPackageBuf, encryptionInfo) + return decryptPackage(packageKey, encryptedPackageBuf, encryptionInfo) } // convertPasswdToKey convert the password into an encryption key. @@ -519,30 +485,21 @@ func parseEncryptionInfo(encryptionInfo []byte) (encryption Encryption, err erro return } -// crypt encrypt / decrypt input by given cipher algorithm, cipher chaining, -// key and initialization vector. -func crypt(encrypt bool, cipherAlgorithm, cipherChaining string, key, iv, input []byte) (packageKey []byte, err error) { +// decrypt provides a function to decrypt input by given cipher algorithm, +// cipher chaining, key and initialization vector. +func decrypt(key, iv, input []byte) (packageKey []byte, err error) { block, err := aes.NewCipher(key) if err != nil { return input, err } - var stream cipher.BlockMode - if encrypt { - stream = cipher.NewCBCEncrypter(block, iv) - } else { - stream = cipher.NewCBCDecrypter(block, iv) - } - stream.CryptBlocks(input, input) + cipher.NewCBCDecrypter(block, iv).CryptBlocks(input, input) return input, nil } -// cryptPackage encrypt / decrypt package by given packageKey and encryption +// decryptPackage decrypt package by given packageKey and encryption // info. -func cryptPackage(encrypt bool, packageKey, input []byte, encryption Encryption) (outputChunks []byte, err error) { +func decryptPackage(packageKey, input []byte, encryption Encryption) (outputChunks []byte, err error) { encryptedKey, offset := encryption.KeyData, packageOffset - if encrypt { - offset = 0 - } var i, start, end int var iv, outputChunk []byte for end < len(input) { @@ -570,17 +527,14 @@ func cryptPackage(encrypt bool, packageKey, input []byte, encryption Encryption) if err != nil { return } - // Encrypt/decrypt the chunk and add it to the array - outputChunk, err = crypt(encrypt, encryptedKey.CipherAlgorithm, encryptedKey.CipherChaining, packageKey, iv, inputChunk) + // Decrypt the chunk and add it to the array + outputChunk, err = decrypt(packageKey, iv, inputChunk) if err != nil { return } outputChunks = append(outputChunks, outputChunk...) i++ } - if encrypt { - outputChunks = append(createUInt32LEBuffer(len(input), 8), outputChunks...) - } return } @@ -662,3 +616,662 @@ func genISOPasswdHash(passwd, hashAlgorithm, salt string, spinCount int) (hashVa hashValue, saltValue = base64.StdEncoding.EncodeToString(key), base64.StdEncoding.EncodeToString(s) return } + +// Compound File Binary Implements + +// cfb structure is used for the compound file binary (CFB) file format writer. +type cfb struct { + stream []byte + position int +} + +// writeBytes write bytes in the stream by a given value with an offset. +func (c *cfb) writeBytes(value []byte) { + pos := c.position + for i := 0; i < len(value); i++ { + for j := len(c.stream); j <= i+pos; j++ { + c.stream = append(c.stream, 0) + } + c.stream[i+pos] = value[i] + } + c.position = pos + len(value) +} + +// writeUint16 write an uint16 data type bytes in the stream by a given value +// with an offset. +func (c *cfb) writeUint16(value int) { + buf := make([]byte, 2) + binary.LittleEndian.PutUint16(buf, uint16(value)) + c.writeBytes(buf) +} + +// writeUint32 write an uint32 data type bytes in the stream by a given value +// with an offset. +func (c *cfb) writeUint32(value int) { + buf := make([]byte, 4) + binary.LittleEndian.PutUint32(buf, uint32(value)) + c.writeBytes(buf) +} + +// writeUint64 write an uint64 data type bytes in the stream by a given value +// with an offset. +func (c *cfb) writeUint64(value int) { + buf := make([]byte, 8) + binary.LittleEndian.PutUint64(buf, uint64(value)) + c.writeBytes(buf) +} + +// writeBytes write strings in the stream by a given value with an offset. +func (c *cfb) writeStrings(value string) { + encoder := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder() + buffer, err := encoder.Bytes([]byte(value)) + if err != nil { + return + } + c.writeBytes(buffer) +} + +// writeVersionStream provides a function to write compound file version +// stream. +func (c *cfb) writeVersionStream() []byte { + var storage cfb + storage.writeUint32(0x3c) + storage.writeStrings("Microsoft.Container.DataSpaces") + storage.writeUint32(0x01) + storage.writeUint32(0x01) + storage.writeUint32(0x01) + return storage.stream +} + +// writeDataSpaceMapStream provides a function to write compound file +// DataSpaceMap stream. +func (c *cfb) writeDataSpaceMapStream() []byte { + var storage cfb + storage.writeUint32(0x08) + storage.writeUint32(0x01) + storage.writeUint32(0x68) + storage.writeUint32(0x01) + storage.writeUint32(0x00) + storage.writeUint32(0x20) + storage.writeStrings("EncryptedPackage") + storage.writeUint32(0x32) + storage.writeStrings("StrongEncryptionDataSpace") + storage.writeUint16(0x00) + return storage.stream +} + +// writeStrongEncryptionDataSpaceStream provides a function to write compound +// file StrongEncryptionDataSpace stream. +func (c *cfb) writeStrongEncryptionDataSpaceStream() []byte { + var storage cfb + storage.writeUint32(0x08) + storage.writeUint32(0x01) + storage.writeUint32(0x32) + storage.writeStrings("StrongEncryptionTransform") + storage.writeUint16(0x00) + return storage.stream +} + +// writePrimaryStream provides a function to write compound file Primary +// stream. +func (c *cfb) writePrimaryStream() []byte { + var storage cfb + storage.writeUint32(0x6C) + storage.writeUint32(0x01) + storage.writeUint32(0x4C) + storage.writeStrings("{FF9A3F03-56EF-4613-BDD5-5A41C1D07246}") + storage.writeUint32(0x4E) + storage.writeUint16(0x00) + storage.writeUint32(0x01) + storage.writeUint32(0x01) + storage.writeUint32(0x01) + storage.writeStrings("AES128") + storage.writeUint32(0x00) + storage.writeUint32(0x04) + return storage.stream +} + +// writeFileStream provides a function to write encrypted package in compound +// file by a given buffer and the short sector allocation table. +func (c *cfb) writeFileStream(encryptionInfoBuffer []byte, SSAT []int) ([]byte, []int) { + var ( + storage cfb + miniProperties int + stream = make([]byte, 0x100) + ) + if encryptionInfoBuffer != nil { + copy(stream, encryptionInfoBuffer) + } + storage.writeBytes(stream) + streamBlocks := len(stream) / 64 + if len(stream)%64 > 0 { + streamBlocks++ + } + for i := 1; i < streamBlocks; i++ { + SSAT = append(SSAT, i) + } + SSAT = append(SSAT, -2) + miniProperties += streamBlocks + versionStream := make([]byte, 0x80) + version := c.writeVersionStream() + copy(versionStream, version) + storage.writeBytes(versionStream) + versionBlocks := len(versionStream) / 64 + if len(versionStream)%64 > 0 { + versionBlocks++ + } + for i := 1; i < versionBlocks; i++ { + SSAT = append(SSAT, i+miniProperties) + } + SSAT = append(SSAT, -2) + miniProperties += versionBlocks + dataSpaceMap := make([]byte, 0x80) + dataStream := c.writeDataSpaceMapStream() + copy(dataSpaceMap, dataStream) + storage.writeBytes(dataSpaceMap) + dataSpaceMapBlocks := len(dataSpaceMap) / 64 + if len(dataSpaceMap)%64 > 0 { + dataSpaceMapBlocks++ + } + for i := 1; i < dataSpaceMapBlocks; i++ { + SSAT = append(SSAT, i+miniProperties) + } + SSAT = append(SSAT, -2) + miniProperties += dataSpaceMapBlocks + dataSpaceStream := c.writeStrongEncryptionDataSpaceStream() + storage.writeBytes(dataSpaceStream) + dataSpaceStreamBlocks := len(dataSpaceStream) / 64 + if len(dataSpaceStream)%64 > 0 { + dataSpaceStreamBlocks++ + } + for i := 1; i < dataSpaceStreamBlocks; i++ { + SSAT = append(SSAT, i+miniProperties) + } + SSAT = append(SSAT, -2) + miniProperties += dataSpaceStreamBlocks + primaryStream := make([]byte, 0x1C0) + primary := c.writePrimaryStream() + copy(primaryStream, primary) + storage.writeBytes(primaryStream) + primaryBlocks := len(primary) / 64 + if len(primary)%64 > 0 { + primaryBlocks++ + } + for i := 1; i < primaryBlocks; i++ { + SSAT = append(SSAT, i+miniProperties) + } + SSAT = append(SSAT, -2) + if len(SSAT) < 128 { + for i := len(SSAT); i < 128; i++ { + SSAT = append(SSAT, -1) + } + } + storage.position = 0 + return storage.stream, SSAT +} + +// writeRootEntry provides a function to write compound file root directory +// entry. The first entry in the first sector of the directory chain +// (also referred to as the first element of the directory array, or stream +// ID #0) is known as the root directory entry, and it is reserved for two +// purposes. First, it provides a root parent for all objects that are +// stationed at the root of the compound file. Second, its function is +// overloaded to store the size and starting sector for the mini stream. +func (c *cfb) writeRootEntry(customSectID int) []byte { + storage := cfb{stream: make([]byte, 128)} + storage.writeStrings("Root Entry") + storage.position = 0x40 + storage.writeUint16(0x16) + storage.writeBytes([]byte{5, 0}) + storage.writeUint32(-1) + storage.writeUint32(-1) + storage.writeUint32(1) + storage.position = 0x64 + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(customSectID) + storage.writeUint32(0x340) + return storage.stream +} + +// writeEncryptionInfo provides a function to write compound file +// writeEncryptionInfo stream. The writeEncryptionInfo stream contains +// detailed information that is used to initialize the cryptography used to +// encrypt the EncryptedPackage stream. +func (c *cfb) writeEncryptionInfo() []byte { + storage := cfb{stream: make([]byte, 128)} + storage.writeStrings("EncryptionInfo") + storage.position = 0x40 + storage.writeUint16(0x1E) + storage.writeBytes([]byte{2, 1}) + storage.writeUint32(0x03) + storage.writeUint32(0x02) + storage.writeUint32(-1) + storage.position = 0x64 + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0xF8) + return storage.stream +} + +// writeEncryptedPackage provides a function to write compound file +// writeEncryptedPackage stream. The writeEncryptedPackage stream is an +// encrypted stream of bytes containing the entire ECMA-376 source file in +// compressed form. +func (c *cfb) writeEncryptedPackage(propertyCount, size int) []byte { + storage := cfb{stream: make([]byte, 128)} + storage.writeStrings("EncryptedPackage") + storage.position = 0x40 + storage.writeUint16(0x22) + storage.writeBytes([]byte{2, 0}) + storage.writeUint32(-1) + storage.writeUint32(-1) + storage.writeUint32(-1) + storage.position = 0x64 + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(propertyCount) + storage.writeUint32(size) + return storage.stream +} + +// writeDataSpaces provides a function to write compound file writeDataSpaces +// stream. The data spaces structure consists of a set of interrelated +// storages and streams in an OLE compound file. +func (c *cfb) writeDataSpaces() []byte { + storage := cfb{stream: make([]byte, 128)} + storage.writeUint16(0x06) + storage.position = 0x40 + storage.writeUint16(0x18) + storage.writeBytes([]byte{1, 0}) + storage.writeUint32(-1) + storage.writeUint32(-1) + storage.writeUint32(5) + storage.position = 0x64 + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + return storage.stream +} + +// writeVersion provides a function to write compound file version. The +// writeVersion structure specifies the version of a product or feature. It +// contains a major and a minor version number. +func (c *cfb) writeVersion() []byte { + storage := cfb{stream: make([]byte, 128)} + storage.writeStrings("Version") + storage.position = 0x40 + storage.writeUint16(0x10) + storage.writeBytes([]byte{2, 1}) + storage.writeUint32(-1) + storage.writeUint32(-1) + storage.writeUint32(-1) + storage.position = 0x64 + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(4) + storage.writeUint32(76) + return storage.stream +} + +// writeDataSpaceMap provides a function to write compound file +// writeDataSpaceMap stream. The writeDataSpaceMap structure associates +// protected content with data space definitions. The data space definition, +// in turn, describes the series of transforms that MUST be applied to that +// protected content to restore it to its original form. By using a map to +// associate data space definitions with content, a single data space +// definition can be used to define the transforms applied to more than one +// piece of protected content. However, a given piece of protected content can +// be referenced only by a single data space definition. +func (c *cfb) writeDataSpaceMap() []byte { + storage := cfb{stream: make([]byte, 128)} + storage.writeStrings("DataSpaceMap") + storage.position = 0x40 + storage.writeUint16(0x1A) + storage.writeBytes([]byte{2, 1}) + storage.writeUint32(0x04) + storage.writeUint32(0x06) + storage.writeUint32(-1) + storage.position = 0x64 + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(6) + storage.writeUint32(112) + return storage.stream +} + +// writeDataSpaceInfo provides a function to write compound file +// writeDataSpaceInfo storage. The writeDataSpaceInfo is a storage containing +// the data space definitions used in the file. This storage must contain one +// or more streams, each of which contains a DataSpaceDefinition structure. +// The storage must contain exactly one stream for each DataSpaceMapEntry +// structure in the DataSpaceMap stream. The name of each stream must be equal +// to the DataSpaceName field of exactly one DataSpaceMapEntry structure +// contained in the DataSpaceMap stream. +func (c *cfb) writeDataSpaceInfo() []byte { + storage := cfb{stream: make([]byte, 128)} + storage.writeStrings("DataSpaceInfo") + storage.position = 0x40 + storage.writeUint16(0x1C) + storage.writeBytes([]byte{1, 1}) + storage.writeUint32(-1) + storage.writeUint32(8) + storage.writeUint32(7) + storage.position = 0x64 + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + return storage.stream +} + +// writeStrongEncryptionDataSpace provides a function to write compound file +// writeStrongEncryptionDataSpace stream. +func (c *cfb) writeStrongEncryptionDataSpace() []byte { + storage := cfb{stream: make([]byte, 128)} + storage.writeStrings("StrongEncryptionDataSpace") + storage.position = 0x40 + storage.writeUint16(0x34) + storage.writeBytes([]byte{2, 1}) + storage.writeUint32(-1) + storage.writeUint32(-1) + storage.writeUint32(-1) + storage.position = 0x64 + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(8) + storage.writeUint32(64) + return storage.stream +} + +// writeTransformInfo provides a function to write compound file +// writeTransformInfo storage. writeTransformInfo is a storage containing +// definitions for the transforms used in the data space definitions stored in +// the DataSpaceInfo storage. The stream contains zero or more definitions for +// the possible transforms that can be applied to the data in content +// streams. +func (c *cfb) writeTransformInfo() []byte { + storage := cfb{stream: make([]byte, 128)} + storage.writeStrings("TransformInfo") + storage.position = 0x40 + storage.writeUint16(0x1C) + storage.writeBytes([]byte{1, 0}) + storage.writeUint32(-1) + storage.writeUint32(-1) + storage.writeUint32(9) + storage.position = 0x64 + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + return storage.stream +} + +// writeStrongEncryptionTransform provides a function to write compound file +// writeStrongEncryptionTransform storage. +func (c *cfb) writeStrongEncryptionTransform() []byte { + storage := cfb{stream: make([]byte, 128)} + storage.writeStrings("StrongEncryptionTransform") + storage.position = 0x40 + storage.writeUint16(0x34) + storage.writeBytes([]byte{1}) + storage.writeBytes([]byte{1}) + storage.writeUint32(-1) + storage.writeUint32(-1) + storage.writeUint32(0x0A) + storage.position = 0x64 + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + return storage.stream +} + +// writePrimary provides a function to write compound file writePrimary stream. +func (c *cfb) writePrimary() []byte { + storage := cfb{stream: make([]byte, 128)} + storage.writeUint16(0x06) + storage.writeStrings("Primary") + storage.position = 0x40 + storage.writeUint16(0x12) + storage.writeBytes([]byte{2, 1}) + storage.writeUint32(-1) + storage.writeUint32(-1) + storage.writeUint32(-1) + storage.position = 0x64 + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(9) + storage.writeUint32(208) + return storage.stream +} + +// writeNoneDir provides a function to write compound file writeNoneDir stream. +func (c *cfb) writeNoneDir() []byte { + storage := cfb{stream: make([]byte, 128)} + storage.position = 0x40 + storage.writeUint16(0x00) + storage.writeUint16(0x00) + storage.writeUint32(-1) + storage.writeUint32(-1) + storage.writeUint32(-1) + return storage.stream +} + +// writeDirectoryEntry provides a function to write compound file directory +// entries. The directory entry array is an array of directory entries that +// are grouped into a directory sector. Each storage object or stream object +// within a compound file is represented by a single directory entry. The +// space for the directory sectors that are holding the array is allocated +// from the FAT. +func (c *cfb) writeDirectoryEntry(propertyCount, customSectID, size int) []byte { + var storage cfb + if size < 0 { + size = 0 + } + for _, entry := range [][]byte{ + c.writeRootEntry(customSectID), + c.writeEncryptionInfo(), + c.writeEncryptedPackage(propertyCount, size), + c.writeDataSpaces(), + c.writeVersion(), + c.writeDataSpaceMap(), + c.writeDataSpaceInfo(), + c.writeStrongEncryptionDataSpace(), + c.writeTransformInfo(), + c.writeStrongEncryptionTransform(), + c.writePrimary(), + c.writeNoneDir(), + } { + storage.writeBytes(entry) + } + return storage.stream +} + +// writeMSAT provides a function to write compound file sector allocation +// table. +func (c *cfb) writeMSAT(MSATBlocks, SATBlocks int, MSAT []int) []int { + if MSATBlocks > 0 { + cnt, MSATIdx := MSATBlocks*128+109, 0 + for i := 0; i < cnt; i++ { + if i < SATBlocks { + bufferSize := i - 109 + if bufferSize > 0 && bufferSize%0x80 == 0 { + MSATIdx++ + MSAT = append(MSAT, MSATIdx) + } + MSAT = append(MSAT, i+MSATBlocks) + continue + } + MSAT = append(MSAT, -1) + } + } else { + for i := 0; i < 109; i++ { + if i < SATBlocks { + MSAT = append(MSAT, i) + continue + } + MSAT = append(MSAT, -1) + } + } + return MSAT +} + +// writeSAT provides a function to write compound file master sector allocation +// table. +func (c *cfb) writeSAT(MSATBlocks, SATBlocks, SSATBlocks, directoryBlocks, fileBlocks, streamBlocks int, SAT []int) (int, []int) { + var blocks int + if SATBlocks > 0 { + for i := 1; i <= MSATBlocks; i++ { + SAT = append(SAT, -4) + } + blocks = MSATBlocks + for i := 1; i <= SATBlocks; i++ { + SAT = append(SAT, -3) + } + blocks += SATBlocks + for i := 1; i < SSATBlocks; i++ { + SAT = append(SAT, i) + } + SAT = append(SAT, -2) + blocks += SSATBlocks + for i := 1; i < directoryBlocks; i++ { + SAT = append(SAT, i+blocks) + } + SAT = append(SAT, -2) + blocks += directoryBlocks + for i := 1; i < fileBlocks; i++ { + SAT = append(SAT, i+blocks) + } + SAT = append(SAT, -2) + blocks += fileBlocks + for i := 1; i < streamBlocks; i++ { + SAT = append(SAT, i+blocks) + } + SAT = append(SAT, -2) + } + return blocks, SAT +} + +// Writer provides a function to create compound file with given info stream +// and package stream. +// +// MSAT - The master sector allocation table +// SSAT - The short sector allocation table +// SAT - The sector allocation table +// +func (c *cfb) Writer(encryptionInfoBuffer, encryptedPackage []byte) []byte { + var ( + storage cfb + MSAT, SAT, SSAT []int + directoryBlocks, fileBlocks, SSATBlocks = 3, 2, 1 + size = int(math.Max(float64(len(encryptedPackage)), float64(packageEncryptionChunkSize))) + streamBlocks = len(encryptedPackage) / 0x200 + ) + if len(encryptedPackage)%0x200 > 0 { + streamBlocks++ + } + propertyBlocks := directoryBlocks + fileBlocks + SSATBlocks + blockSize := (streamBlocks + propertyBlocks) * 4 + SATBlocks := blockSize / 0x200 + if blockSize%0x200 > 0 { + SATBlocks++ + } + MSATBlocks, blocksChanged := 0, true + for blocksChanged { + var SATCap, MSATCap int + blocksChanged = false + blockSize = (streamBlocks + propertyBlocks + SATBlocks + MSATBlocks) * 4 + SATCap = blockSize / 0x200 + if blockSize%0x200 > 0 { + SATCap++ + } + if SATCap > SATBlocks { + SATBlocks, blocksChanged = SATCap, true + continue + } + if SATBlocks > 109 { + blockRemains := (SATBlocks - 109) * 4 + blockBuffer := blockRemains % 0x200 + MSATCap = blockRemains / 0x200 + if blockBuffer > 0 { + MSATCap++ + } + if blockBuffer+(4*MSATCap) > 0x200 { + MSATCap++ + } + if MSATCap > MSATBlocks { + MSATBlocks, blocksChanged = MSATCap, true + } + } + } + MSAT = c.writeMSAT(MSATBlocks, SATBlocks, MSAT) + blocks, SAT := c.writeSAT(MSATBlocks, SATBlocks, SSATBlocks, directoryBlocks, fileBlocks, streamBlocks, SAT) + storage.writeUint32(0xE011CFD0) + storage.writeUint32(0xE11AB1A1) + storage.writeUint64(0x00) + storage.writeUint64(0x00) + storage.writeUint16(0x003E) + storage.writeUint16(0x0003) + storage.writeUint16(-2) + storage.writeUint16(9) + storage.writeUint32(6) + storage.writeUint32(0) + storage.writeUint32(0) + storage.writeUint32(SATBlocks) + storage.writeUint32(MSATBlocks + SATBlocks + SSATBlocks) + storage.writeUint32(0) + storage.writeUint32(0x00001000) + storage.writeUint32(SATBlocks + MSATBlocks) + storage.writeUint32(SSATBlocks) + if MSATBlocks > 0 { + storage.writeUint32(0) + storage.writeUint32(MSATBlocks) + } else { + storage.writeUint32(-2) + storage.writeUint32(0) + } + for _, block := range MSAT { + storage.writeUint32(block) + } + for i := 0; i < SATBlocks*128; i++ { + if i < len(SAT) { + storage.writeUint32(SAT[i]) + continue + } + storage.writeUint32(-1) + } + fileStream, SSATStream := c.writeFileStream(encryptionInfoBuffer, SSAT) + for _, block := range SSATStream { + storage.writeUint32(block) + } + directoryEntry := c.writeDirectoryEntry(blocks, blocks-fileBlocks, size) + storage.writeBytes(directoryEntry) + storage.writeBytes(fileStream) + storage.writeBytes(encryptedPackage) + return storage.stream +} diff --git a/crypt_test.go b/crypt_test.go index d09517674a8..2df5af2cca2 100644 --- a/crypt_test.go +++ b/crypt_test.go @@ -13,18 +13,33 @@ package excelize import ( "path/filepath" + "strings" "testing" "github.com/stretchr/testify/assert" ) func TestEncrypt(t *testing.T) { + // Test decrypt spreadsheet with incorrect password _, err := OpenFile(filepath.Join("test", "encryptSHA1.xlsx"), Options{Password: "passwd"}) assert.EqualError(t, err, ErrWorkbookPassword.Error()) - + // Test decrypt spreadsheet with password f, err := OpenFile(filepath.Join("test", "encryptSHA1.xlsx"), Options{Password: "password"}) assert.NoError(t, err) - assert.EqualError(t, f.SaveAs(filepath.Join("test", "BadEncrypt.xlsx"), Options{Password: "password"}), ErrEncrypt.Error()) + cell, err := f.GetCellValue("Sheet1", "A1") + assert.NoError(t, err) + assert.Equal(t, "SECRET", cell) + assert.NoError(t, f.Close()) + // Test encrypt spreadsheet with invalid password + assert.EqualError(t, f.SaveAs(filepath.Join("test", "Encryption.xlsx"), Options{Password: strings.Repeat("*", MaxFieldLength+1)}), ErrPasswordLengthInvalid.Error()) + // Test encrypt spreadsheet with new password + assert.NoError(t, f.SaveAs(filepath.Join("test", "Encryption.xlsx"), Options{Password: "passwd"})) + assert.NoError(t, f.Close()) + f, err = OpenFile(filepath.Join("test", "Encryption.xlsx"), Options{Password: "passwd"}) + assert.NoError(t, err) + cell, err = f.GetCellValue("Sheet1", "A1") + assert.NoError(t, err) + assert.Equal(t, "SECRET", cell) assert.NoError(t, f.Close()) } diff --git a/errors.go b/errors.go index 980629314e0..6606f1eaf5b 100644 --- a/errors.go +++ b/errors.go @@ -112,8 +112,6 @@ var ( // ErrMaxFilePathLength defined the error message on receive the file path // length overflow. ErrMaxFilePathLength = errors.New("file path length exceeds maximum limit") - // ErrEncrypt defined the error message on encryption spreadsheet. - ErrEncrypt = errors.New("not support encryption currently") // ErrUnknownEncryptMechanism defined the error message on unsupported // encryption mechanism. ErrUnknownEncryptMechanism = errors.New("unknown encryption mechanism") diff --git a/excelize.go b/excelize.go index 0e2f440ed59..8c71b167e12 100644 --- a/excelize.go +++ b/excelize.go @@ -93,10 +93,6 @@ type Options struct { // return // } // -// Note that the excelize just support decrypt and not support encrypt -// currently, the spreadsheet saved by Save and SaveAs will be without -// password unprotected. Close the file by Close after opening the -// spreadsheet. func OpenFile(filename string, opt ...Options) (*File, error) { file, err := os.Open(filepath.Clean(filename)) if err != nil { diff --git a/file.go b/file.go index ecdadf4c0b1..5931bdb4fbb 100644 --- a/file.go +++ b/file.go @@ -60,6 +60,9 @@ func (f *File) Save() error { if f.Path == "" { return ErrSave } + if f.options != nil { + return f.SaveAs(f.Path, *f.options) + } return f.SaveAs(f.Path) }