Browse Source

init ECMA-376 agile encryption support

xuri 5 years ago
parent
commit
01afc6e03f
4 changed files with 208 additions and 28 deletions
  1. 163 18
      crypt.go
  2. 23 0
      crypt_test.go
  3. 5 9
      excelize.go
  4. 17 1
      file.go

+ 163 - 18
crypt.go

@@ -13,6 +13,7 @@ import (
 	"bytes"
 	"crypto/aes"
 	"crypto/cipher"
+	"crypto/hmac"
 	"crypto/md5"
 	"crypto/sha1"
 	"crypto/sha256"
@@ -22,6 +23,8 @@ import (
 	"encoding/xml"
 	"errors"
 	"hash"
+	"math/rand"
+	"reflect"
 	"strings"
 
 	"github.com/richardlehane/mscfb"
@@ -32,7 +35,11 @@ import (
 
 var (
 	blockKey                   = []byte{0x14, 0x6e, 0x0b, 0xe7, 0xab, 0xac, 0xd0, 0xd6} // Block keys used for encryption
-	packageOffset              = 8                                                      // First 8 bytes are the size of the stream
+	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
 	iterCount                  = 50000
 	cryptoIdentifier           = []byte{ // checking protect workbook by [MS-OFFCRYPTO] - v20181211 3.1 FeatureIdentifier
@@ -50,6 +57,7 @@ var (
 // Encryption specifies the encryption structure, streams, and storages are
 // required when encrypting ECMA-376 documents.
 type Encryption struct {
+	XMLName       xml.Name      `xml:"encryption"`
 	KeyData       KeyData       `xml:"keyData"`
 	DataIntegrity DataIntegrity `xml:"dataIntegrity"`
 	KeyEncryptors KeyEncryptors `xml:"keyEncryptors"`
@@ -150,6 +158,125 @@ func Decrypt(raw []byte, opt *Options) (packageBuf []byte, err error) {
 	return
 }
 
+// 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
+	}
+	// Use the package key and the IV to encrypt the HMAC key.
+	encryptedHmacKey, err := 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, err := 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, err := 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)
+	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
+	}
+	// TODO: Create a new CFB.
+	_, _ = encryptedPackage, encryptionInfoBuffer
+	err = errors.New("not support encryption currently")
+	return
+}
+
 // extractPart extract data from storage by specified part name.
 func extractPart(doc *mscfb.Reader) (encryptionInfoBuf, encryptedPackageBuf []byte) {
 	for entry, err := doc.Next(); err == nil; entry, err = doc.Next() {
@@ -265,11 +392,11 @@ func standardConvertPasswdToKey(header StandardEncryptionHeader, verifier Standa
 	}
 	key := hashing("sha1", verifier.Salt, passwordBuffer)
 	for i := 0; i < iterCount; i++ {
-		iterator := createUInt32LEBuffer(i)
+		iterator := createUInt32LEBuffer(i, 4)
 		key = hashing("sha1", iterator, key)
 	}
 	var block int
-	hfinal := hashing("sha1", key, createUInt32LEBuffer(block))
+	hfinal := hashing("sha1", key, createUInt32LEBuffer(block, 4))
 	cbRequiredKeyLength := int(header.KeySize) / 8
 	cbHash := sha1.Size
 	buf1 := bytes.Repeat([]byte{0x36}, 64)
@@ -299,15 +426,14 @@ func standardXORBytes(a, b []byte) []byte {
 // ECMA-376 Agile Encryption
 
 // agileDecrypt decrypt the CFB file format with ECMA-376 agile encryption.
-// Support cryptographic algorithm: MD4, MD5, RIPEMD-160, SHA1, SHA256, SHA384
-// and SHA512.
+// Support cryptographic algorithm: MD4, MD5, RIPEMD-160, SHA1, SHA256, SHA384 and SHA512.
 func agileDecrypt(encryptionInfoBuf, encryptedPackageBuf []byte, opt *Options) (packageBuf []byte, err error) {
 	var encryptionInfo Encryption
 	if encryptionInfo, err = parseEncryptionInfo(encryptionInfoBuf[8:]); err != nil {
 		return
 	}
 	// Convert the password into an encryption key.
-	key, err := convertPasswdToKey(opt.Password, encryptionInfo)
+	key, err := convertPasswdToKey(opt.Password, blockKey, encryptionInfo)
 	if err != nil {
 		return
 	}
@@ -327,7 +453,7 @@ func agileDecrypt(encryptionInfoBuf, encryptedPackageBuf []byte, opt *Options) (
 }
 
 // convertPasswdToKey convert the password into an encryption key.
-func convertPasswdToKey(passwd string, encryption Encryption) (key []byte, err error) {
+func convertPasswdToKey(passwd string, blockKey []byte, encryption Encryption) (key []byte, err error) {
 	var b bytes.Buffer
 	saltValue, err := base64.StdEncoding.DecodeString(encryption.KeyEncryptors.KeyEncryptor[0].EncryptedKey.SaltValue)
 	if err != nil {
@@ -344,7 +470,7 @@ func convertPasswdToKey(passwd string, encryption Encryption) (key []byte, err e
 	key = hashing(encryption.KeyData.HashAlgorithm, b.Bytes())
 	// Now regenerate until spin count.
 	for i := 0; i < encryption.KeyEncryptors.KeyEncryptor[0].EncryptedKey.SpinCount; i++ {
-		iterator := createUInt32LEBuffer(i)
+		iterator := createUInt32LEBuffer(i, 4)
 		key = hashing(encryption.KeyData.HashAlgorithm, iterator, key)
 	}
 	// Now generate the final hash.
@@ -385,8 +511,8 @@ func hashing(hashAlgorithm string, buffer ...[]byte) (key []byte) {
 
 // createUInt32LEBuffer create buffer with little endian 32-bit unsigned
 // integer.
-func createUInt32LEBuffer(value int) []byte {
-	buf := make([]byte, 4)
+func createUInt32LEBuffer(value int, bufferSize int) []byte {
+	buf := make([]byte, bufferSize)
 	binary.LittleEndian.PutUint32(buf, uint32(value))
 	return buf
 }
@@ -404,7 +530,12 @@ func crypt(encrypt bool, cipherAlgorithm, cipherChaining string, key, iv, input
 	if err != nil {
 		return input, err
 	}
-	stream := cipher.NewCBCDecrypter(block, iv)
+	var stream cipher.BlockMode
+	if encrypt {
+		stream = cipher.NewCBCEncrypter(block, iv)
+	} else {
+		stream = cipher.NewCBCDecrypter(block, iv)
+	}
 	stream.CryptBlocks(input, input)
 	return input, nil
 }
@@ -440,7 +571,7 @@ func cryptPackage(encrypt bool, packageKey, input []byte, encryption Encryption)
 			inputChunk = append(inputChunk, make([]byte, encryptedKey.BlockSize-remainder)...)
 		}
 		// Create the initialization vector
-		iv, err = createIV(encrypt, i, encryption)
+		iv, err = createIV(i, encryption)
 		if err != nil {
 			return
 		}
@@ -452,24 +583,29 @@ func cryptPackage(encrypt bool, packageKey, input []byte, encryption Encryption)
 		outputChunks = append(outputChunks, outputChunk...)
 		i++
 	}
+	if encrypt {
+		outputChunks = append(createUInt32LEBuffer(len(input), 8), outputChunks...)
+	}
 	return
 }
 
 // createIV create an initialization vector (IV).
-func createIV(encrypt bool, blockKey int, encryption Encryption) ([]byte, error) {
+func createIV(blockKey interface{}, encryption Encryption) ([]byte, error) {
 	encryptedKey := encryption.KeyData
 	// Create the block key from the current index
-	blockKeyBuf := createUInt32LEBuffer(blockKey)
-	var b bytes.Buffer
+	var blockKeyBuf []byte
+	if reflect.TypeOf(blockKey).Kind() == reflect.Int {
+		blockKeyBuf = createUInt32LEBuffer(blockKey.(int), 4)
+	} else {
+		blockKeyBuf = blockKey.([]byte)
+	}
 	saltValue, err := base64.StdEncoding.DecodeString(encryptedKey.SaltValue)
 	if err != nil {
 		return nil, err
 	}
-	b.Write(saltValue)
-	b.Write(blockKeyBuf)
 	// Create the initialization vector by hashing the salt with the block key.
 	// Truncate or pad as needed to meet the block size.
-	iv := hashing(encryptedKey.HashAlgorithm, b.Bytes())
+	iv := hashing(encryptedKey.HashAlgorithm, append(saltValue, blockKeyBuf...))
 	if len(iv) < encryptedKey.BlockSize {
 		tmp := make([]byte, 0x36)
 		iv = append(iv, tmp...)
@@ -479,3 +615,12 @@ func createIV(encrypt bool, blockKey int, encryption Encryption) ([]byte, error)
 	}
 	return iv, nil
 }
+
+// randomBytes returns securely generated random bytes. It will return an error if the system's
+// secure random number generator fails to function correctly, in which case the caller should not
+// continue.
+func randomBytes(n int) ([]byte, error) {
+	b := make([]byte, n)
+	_, err := rand.Read(b)
+	return b, err
+}

+ 23 - 0
crypt_test.go

@@ -0,0 +1,23 @@
+// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
+// this source code is governed by a BSD-style license that can be found in
+// the LICENSE file.
+//
+// Package excelize providing a set of functions that allow you to write to
+// and read from XLSX files. Support reads and writes XLSX file generated by
+// Microsoft Excel™ 2007 and later. Support save file without losing original
+// charts of XLSX. This library needs Go version 1.10 or later.
+
+package excelize
+
+import (
+	"path/filepath"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestEncrypt(t *testing.T) {
+	f, err := OpenFile(filepath.Join("test", "encryptSHA1.xlsx"), Options{Password: "password"})
+	assert.NoError(t, err)
+	assert.EqualError(t, f.SaveAs(filepath.Join("test", "TestEncrypt.xlsx"), Options{Password: "password"}), "not support encryption currently")
+}

+ 5 - 9
excelize.go

@@ -32,6 +32,7 @@ import (
 // File define a populated spreadsheet file struct.
 type File struct {
 	sync.Mutex
+	options          *Options
 	xmlAttr          map[string][]xml.Attr
 	checked          map[string]bool
 	sheetMap         map[string]string
@@ -75,11 +76,7 @@ func OpenFile(filename string, opt ...Options) (*File, error) {
 		return nil, err
 	}
 	defer file.Close()
-	var option Options
-	for _, o := range opt {
-		option = o
-	}
-	f, err := OpenReader(file, option)
+	f, err := OpenReader(file, opt...)
 	if err != nil {
 		return nil, err
 	}
@@ -111,12 +108,12 @@ func OpenReader(r io.Reader, opt ...Options) (*File, error) {
 	if err != nil {
 		return nil, err
 	}
+	f := newFile()
 	if bytes.Contains(b, oleIdentifier) {
-		var option Options
 		for _, o := range opt {
-			option = o
+			f.options = &o
 		}
-		b, err = Decrypt(b, &option)
+		b, err = Decrypt(b, f.options)
 		if err != nil {
 			return nil, fmt.Errorf("decrypted file failed")
 		}
@@ -130,7 +127,6 @@ func OpenReader(r io.Reader, opt ...Options) (*File, error) {
 	if err != nil {
 		return nil, err
 	}
-	f := newFile()
 	f.SheetCount, f.XLSX = sheetCount, file
 	f.CalcChain = f.calcChainReader()
 	f.sheetMap = f.getSheetMap()

+ 17 - 1
file.go

@@ -64,7 +64,7 @@ func (f *File) Save() error {
 
 // SaveAs provides a function to create or update to an xlsx file at the
 // provided path.
-func (f *File) SaveAs(name string) error {
+func (f *File) SaveAs(name string, opt ...Options) error {
 	if len(name) > FileNameLength {
 		return errors.New("file name length exceeds maximum limit")
 	}
@@ -73,6 +73,9 @@ func (f *File) SaveAs(name string) error {
 		return err
 	}
 	defer file.Close()
+	for _, o := range opt {
+		f.options = &o
+	}
 	return f.Write(file)
 }
 
@@ -118,5 +121,18 @@ func (f *File) WriteToBuffer() (*bytes.Buffer, error) {
 			return buf, err
 		}
 	}
+
+	if f.options != nil {
+		if err := zw.Close(); err != nil {
+			return buf, err
+		}
+		b, err := Encrypt(buf.Bytes(), f.options)
+		if err != nil {
+			return buf, err
+		}
+		buf.Reset()
+		buf.Write(b)
+		return buf, nil
+	}
 	return buf, zw.Close()
 }