support ECMA-376 document standard encryption, ref #199
This commit is contained in:
parent
4177c1585e
commit
98f1a69903
|
@ -20,6 +20,7 @@ import (
|
|||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"hash"
|
||||
"strings"
|
||||
|
||||
|
@ -33,6 +34,7 @@ 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
|
||||
packageEncryptionChunkSize = 4096
|
||||
iterCount = 50000
|
||||
cryptoIdentifier = []byte{ // checking protect workbook by [MS-OFFCRYPTO] - v20181211 3.1 FeatureIdentifier
|
||||
0x3c, 0x00, 0x00, 0x00, 0x4d, 0x00, 0x69, 0x00, 0x63, 0x00, 0x72, 0x00, 0x6f, 0x00, 0x73, 0x00,
|
||||
0x6f, 0x00, 0x66, 0x00, 0x74, 0x00, 0x2e, 0x00, 0x43, 0x00, 0x6f, 0x00, 0x6e, 0x00, 0x74, 0x00,
|
||||
|
@ -40,6 +42,9 @@ var (
|
|||
0x74, 0x00, 0x61, 0x00, 0x53, 0x00, 0x70, 0x00, 0x61, 0x00, 0x63, 0x00, 0x65, 0x00, 0x73, 0x00,
|
||||
0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
|
||||
}
|
||||
oleIdentifier = []byte{
|
||||
0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1,
|
||||
}
|
||||
)
|
||||
|
||||
// Encryption specifies the encryption structure, streams, and storages are
|
||||
|
@ -93,37 +98,56 @@ type EncryptedKey struct {
|
|||
KeyData
|
||||
}
|
||||
|
||||
// Decrypt API decrypt the CFB file format with Agile Encryption. Support
|
||||
// cryptographic algorithm: MD4, MD5, RIPEMD-160, SHA1, SHA256, SHA384 and
|
||||
// SHA512.
|
||||
// StandardEncryptionHeader structure is used by ECMA-376 document encryption
|
||||
// [ECMA-376] and Office binary document RC4 CryptoAPI encryption, to specify
|
||||
// encryption properties for an encrypted stream.
|
||||
type StandardEncryptionHeader struct {
|
||||
Flags uint32
|
||||
SizeExtra uint32
|
||||
AlgID uint32
|
||||
AlgIDHash uint32
|
||||
KeySize uint32
|
||||
ProviderType uint32
|
||||
Reserved1 uint32
|
||||
Reserved2 uint32
|
||||
CspName string
|
||||
}
|
||||
|
||||
// StandardEncryptionVerifier structure is used by Office Binary Document RC4
|
||||
// CryptoAPI Encryption and ECMA-376 Document Encryption. Every usage of this
|
||||
// structure MUST specify the hashing algorithm and encryption algorithm used
|
||||
// in the EncryptionVerifier structure.
|
||||
type StandardEncryptionVerifier struct {
|
||||
SaltSize uint32
|
||||
Salt []byte
|
||||
EncryptedVerifier []byte
|
||||
VerifierHashSize uint32
|
||||
EncryptedVerifierHash []byte
|
||||
}
|
||||
|
||||
// Decrypt API decrypt 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.
|
||||
func Decrypt(raw []byte, opt *Options) (packageBuf []byte, err error) {
|
||||
doc, err := mscfb.New(bytes.NewReader(raw))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
encryptionInfoBuf, encryptedPackageBuf := extractPart(doc)
|
||||
var encryptionInfo Encryption
|
||||
if encryptionInfo, err = parseEncryptionInfo(encryptionInfoBuf[8:]); err != nil {
|
||||
mechanism, err := encryptionMechanism(encryptionInfoBuf)
|
||||
if err != nil || mechanism == "extensible" {
|
||||
return
|
||||
}
|
||||
// Convert the password into an encryption key.
|
||||
key, err := convertPasswdToKey(opt.Password, encryptionInfo)
|
||||
if err != nil {
|
||||
return
|
||||
switch mechanism {
|
||||
case "agile":
|
||||
return agileDecrypt(encryptionInfoBuf, encryptedPackageBuf, opt)
|
||||
case "standard":
|
||||
return standardDecrypt(encryptionInfoBuf, encryptedPackageBuf, opt)
|
||||
default:
|
||||
err = errors.New("unsupport encryption mechanism")
|
||||
break
|
||||
}
|
||||
// Use the key to decrypt the package key.
|
||||
encryptedKey := encryptionInfo.KeyEncryptors.KeyEncryptor[0].EncryptedKey
|
||||
saltValue, err := base64.StdEncoding.DecodeString(encryptedKey.SaltValue)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
encryptedKeyValue, err := base64.StdEncoding.DecodeString(encryptedKey.EncryptedKeyValue)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
packageKey, err := crypt(false, encryptedKey.CipherAlgorithm, encryptedKey.CipherChaining, key, saltValue, encryptedKeyValue)
|
||||
// Use the package key to decrypt the package.
|
||||
return cryptPackage(false, packageKey, encryptedPackageBuf, encryptionInfo)
|
||||
return
|
||||
}
|
||||
|
||||
// extractPart extract data from storage by specified part name.
|
||||
|
@ -149,6 +173,159 @@ func extractPart(doc *mscfb.Reader) (encryptionInfoBuf, encryptedPackageBuf []by
|
|||
return
|
||||
}
|
||||
|
||||
// encryptionMechanism parse password-protected documents created mechanism.
|
||||
func encryptionMechanism(buffer []byte) (mechanism string, err error) {
|
||||
if len(buffer) < 4 {
|
||||
err = errors.New("unknown encryption mechanism")
|
||||
return
|
||||
}
|
||||
versionMajor, versionMinor := binary.LittleEndian.Uint16(buffer[0:2]), binary.LittleEndian.Uint16(buffer[2:4])
|
||||
if versionMajor == 4 && versionMinor == 4 {
|
||||
mechanism = "agile"
|
||||
return
|
||||
} else if (2 <= versionMajor && versionMajor <= 4) && versionMinor == 2 {
|
||||
mechanism = "standard"
|
||||
return
|
||||
} else if (versionMajor == 3 || versionMajor == 4) && versionMinor == 3 {
|
||||
mechanism = "extensible"
|
||||
}
|
||||
err = errors.New("unsupport encryption mechanism")
|
||||
return
|
||||
}
|
||||
|
||||
// ECMA-376 Standard Encryption
|
||||
|
||||
// standardDecrypt decrypt the CFB file format with ECMA-376 standard encryption.
|
||||
func standardDecrypt(encryptionInfoBuf, encryptedPackageBuf []byte, opt *Options) ([]byte, error) {
|
||||
encryptionHeaderSize := binary.LittleEndian.Uint32(encryptionInfoBuf[8:12])
|
||||
block := encryptionInfoBuf[12 : 12+encryptionHeaderSize]
|
||||
header := StandardEncryptionHeader{
|
||||
Flags: binary.LittleEndian.Uint32(block[:4]),
|
||||
SizeExtra: binary.LittleEndian.Uint32(block[4:8]),
|
||||
AlgID: binary.LittleEndian.Uint32(block[8:12]),
|
||||
AlgIDHash: binary.LittleEndian.Uint32(block[12:16]),
|
||||
KeySize: binary.LittleEndian.Uint32(block[16:20]),
|
||||
ProviderType: binary.LittleEndian.Uint32(block[20:24]),
|
||||
Reserved1: binary.LittleEndian.Uint32(block[24:28]),
|
||||
Reserved2: binary.LittleEndian.Uint32(block[28:32]),
|
||||
CspName: string(block[32:]),
|
||||
}
|
||||
block = encryptionInfoBuf[12+encryptionHeaderSize:]
|
||||
algIDMap := map[uint32]string{
|
||||
0x0000660E: "AES-128",
|
||||
0x0000660F: "AES-192",
|
||||
0x00006610: "AES-256",
|
||||
}
|
||||
algorithm := "AES"
|
||||
_, ok := algIDMap[header.AlgID]
|
||||
if !ok {
|
||||
algorithm = "RC4"
|
||||
}
|
||||
verifier := standardEncryptionVerifier(algorithm, block)
|
||||
secretKey, err := standardConvertPasswdToKey(header, verifier, opt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// decrypted data
|
||||
x := encryptedPackageBuf[8:]
|
||||
blob, err := aes.NewCipher(secretKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
decrypted := make([]byte, len(x))
|
||||
size := 16
|
||||
for bs, be := 0, size; bs < len(x); bs, be = bs+size, be+size {
|
||||
blob.Decrypt(decrypted[bs:be], x[bs:be])
|
||||
}
|
||||
return decrypted, err
|
||||
}
|
||||
|
||||
// standardEncryptionVerifier extract ECMA-376 standard encryption verifier.
|
||||
func standardEncryptionVerifier(algorithm string, blob []byte) StandardEncryptionVerifier {
|
||||
verifier := StandardEncryptionVerifier{
|
||||
SaltSize: binary.LittleEndian.Uint32(blob[:4]),
|
||||
Salt: blob[4:20],
|
||||
EncryptedVerifier: blob[20:36],
|
||||
VerifierHashSize: binary.LittleEndian.Uint32(blob[36:40]),
|
||||
}
|
||||
if algorithm == "RC4" {
|
||||
verifier.EncryptedVerifierHash = blob[40:60]
|
||||
} else if algorithm == "AES" {
|
||||
verifier.EncryptedVerifierHash = blob[40:72]
|
||||
}
|
||||
return verifier
|
||||
}
|
||||
|
||||
// standardConvertPasswdToKey generate intermediate key from given password.
|
||||
func standardConvertPasswdToKey(header StandardEncryptionHeader, verifier StandardEncryptionVerifier, opt *Options) ([]byte, error) {
|
||||
encoder := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder()
|
||||
passwordBuffer, err := encoder.Bytes([]byte(opt.Password))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key := hashing("sha1", verifier.Salt, passwordBuffer)
|
||||
for i := 0; i < iterCount; i++ {
|
||||
iterator := createUInt32LEBuffer(i)
|
||||
key = hashing("sha1", iterator, key)
|
||||
}
|
||||
var block int
|
||||
hfinal := hashing("sha1", key, createUInt32LEBuffer(block))
|
||||
cbRequiredKeyLength := int(header.KeySize) / 8
|
||||
cbHash := sha1.Size
|
||||
buf1 := bytes.Repeat([]byte{0x36}, 64)
|
||||
buf1 = append(standardXORBytes(hfinal, buf1[:cbHash]), buf1[cbHash:]...)
|
||||
x1 := hashing("sha1", buf1)
|
||||
buf2 := bytes.Repeat([]byte{0x5c}, 64)
|
||||
buf2 = append(standardXORBytes(hfinal, buf2[:cbHash]), buf2[cbHash:]...)
|
||||
x2 := hashing("sha1", buf2)
|
||||
x3 := append(x1, x2...)
|
||||
keyDerived := x3[:cbRequiredKeyLength]
|
||||
return keyDerived, err
|
||||
}
|
||||
|
||||
// standardXORBytes perform XOR operations for two bytes slice.
|
||||
func standardXORBytes(a, b []byte) []byte {
|
||||
r := make([][2]byte, len(a), len(a))
|
||||
for i, e := range a {
|
||||
r[i] = [2]byte{e, b[i]}
|
||||
}
|
||||
buf := make([]byte, len(a))
|
||||
for p, q := range r {
|
||||
buf[p] = q[0] ^ q[1]
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
// 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.
|
||||
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)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// Use the key to decrypt the package key.
|
||||
encryptedKey := encryptionInfo.KeyEncryptors.KeyEncryptor[0].EncryptedKey
|
||||
saltValue, err := base64.StdEncoding.DecodeString(encryptedKey.SaltValue)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
encryptedKeyValue, err := base64.StdEncoding.DecodeString(encryptedKey.EncryptedKeyValue)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
packageKey, err := crypt(false, encryptedKey.CipherAlgorithm, encryptedKey.CipherChaining, key, saltValue, encryptedKeyValue)
|
||||
// Use the package key to decrypt the package.
|
||||
return cryptPackage(false, packageKey, encryptedPackageBuf, encryptionInfo)
|
||||
}
|
||||
|
||||
// convertPasswdToKey convert the password into an encryption key.
|
||||
func convertPasswdToKey(passwd string, encryption Encryption) (key []byte, err error) {
|
||||
var b bytes.Buffer
|
|
@ -59,10 +59,7 @@ func (f *File) prepareChartSheetDrawing(xlsx *xlsxChartsheet, drawingID int, she
|
|||
func (f *File) addChart(formatSet *formatChart, comboCharts []*formatChart) {
|
||||
count := f.countCharts()
|
||||
xlsxChartSpace := xlsxChartSpace{
|
||||
XMLNSc: NameSpaceDrawingMLChart.Value,
|
||||
XMLNSa: NameSpaceDrawingML.Value,
|
||||
XMLNSr: SourceRelationship.Value,
|
||||
XMLNSc16r2: SourceRelationshipChart201506.Value,
|
||||
Date1904: &attrValBool{Val: boolPtr(false)},
|
||||
Lang: &attrValString{Val: stringPtr("en-US")},
|
||||
RoundedCorners: &attrValBool{Val: boolPtr(false)},
|
||||
|
|
|
@ -111,7 +111,7 @@ func OpenReader(r io.Reader, opt ...Options) (*File, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if bytes.Contains(b, cryptoIdentifier) {
|
||||
if bytes.Contains(b, oleIdentifier) {
|
||||
var option Options
|
||||
for _, o := range opt {
|
||||
option = o
|
||||
|
|
|
@ -201,15 +201,23 @@ func TestCharsetTranscoder(t *testing.T) {
|
|||
func TestOpenReader(t *testing.T) {
|
||||
_, err := OpenReader(strings.NewReader(""))
|
||||
assert.EqualError(t, err, "zip: not a valid zip file")
|
||||
_, err = OpenReader(bytes.NewReader(cryptoIdentifier))
|
||||
_, err = OpenReader(bytes.NewReader(oleIdentifier))
|
||||
assert.EqualError(t, err, "decrypted file failed")
|
||||
|
||||
// Test open password protected spreadsheet created by Microsoft Office Excel 2010.
|
||||
f, err := OpenFile(filepath.Join("test", "encryptSHA1.xlsx"), Options{Password: "password"})
|
||||
assert.NoError(t, err)
|
||||
val, err := f.GetCellValue("Sheet1", "A1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "SECRET", val)
|
||||
|
||||
// Test open password protected spreadsheet created by LibreOffice 7.0.0.3.
|
||||
f, err = OpenFile(filepath.Join("test", "encryptAES.xlsx"), Options{Password: "password"})
|
||||
assert.NoError(t, err)
|
||||
val, err = f.GetCellValue("Sheet1", "A1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "SECRET", val)
|
||||
|
||||
// Test unexpected EOF.
|
||||
var b bytes.Buffer
|
||||
w := gzip.NewWriter(&b)
|
||||
|
|
Binary file not shown.
|
@ -18,10 +18,7 @@ import "encoding/xml"
|
|||
// charts, pie charts, scatter charts, or other types of charts.
|
||||
type xlsxChartSpace struct {
|
||||
XMLName xml.Name `xml:"http://schemas.openxmlformats.org/drawingml/2006/chart chartSpace"`
|
||||
XMLNSc string `xml:"xmlns:c,attr"`
|
||||
XMLNSa string `xml:"xmlns:a,attr"`
|
||||
XMLNSr string `xml:"xmlns:r,attr"`
|
||||
XMLNSc16r2 string `xml:"xmlns:c16r2,attr"`
|
||||
Date1904 *attrValBool `xml:"date1904"`
|
||||
Lang *attrValString `xml:"lang"`
|
||||
RoundedCorners *attrValBool `xml:"roundedCorners"`
|
||||
|
|
Loading…
Reference in New Issue