Skip to content

Commit 45c4b72

Browse files
qinyuguangxuri
authored andcommitted
Add support for workbook protection (qax-os#1431)
1 parent e503cdb commit 45c4b72

File tree

5 files changed

+141
-10
lines changed

5 files changed

+141
-10
lines changed

crypt.go

+11-10
Original file line numberDiff line numberDiff line change
@@ -37,16 +37,17 @@ import (
3737
)
3838

3939
var (
40-
blockKey = []byte{0x14, 0x6e, 0x0b, 0xe7, 0xab, 0xac, 0xd0, 0xd6} // Block keys used for encryption
41-
oleIdentifier = []byte{0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1}
42-
headerCLSID = make([]byte, 16)
43-
difSect = -4
44-
endOfChain = -2
45-
fatSect = -3
46-
iterCount = 50000
47-
packageEncryptionChunkSize = 4096
48-
packageOffset = 8 // First 8 bytes are the size of the stream
49-
sheetProtectionSpinCount = 1e5
40+
blockKey = []byte{0x14, 0x6e, 0x0b, 0xe7, 0xab, 0xac, 0xd0, 0xd6} // Block keys used for encryption
41+
oleIdentifier = []byte{0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1}
42+
headerCLSID = make([]byte, 16)
43+
difSect = -4
44+
endOfChain = -2
45+
fatSect = -3
46+
iterCount = 50000
47+
packageEncryptionChunkSize = 4096
48+
packageOffset = 8 // First 8 bytes are the size of the stream
49+
sheetProtectionSpinCount = 1e5
50+
workbookProtectionSpinCount = 1e5
5051
)
5152

5253
// Encryption specifies the encryption structure, streams, and storages are

errors.go

+6
Original file line numberDiff line numberDiff line change
@@ -230,4 +230,10 @@ var (
230230
// ErrSheetNameLength defined the error message on receiving the sheet
231231
// name length exceeds the limit.
232232
ErrSheetNameLength = fmt.Errorf("the sheet name length exceeds the %d characters limit", MaxSheetNameLength)
233+
// ErrUnprotectWorkbook defined the error message on workbook has set no
234+
// protection.
235+
ErrUnprotectWorkbook = errors.New("workbook has set no protect")
236+
// ErrUnprotectWorkbookPassword defined the error message on remove workbook
237+
// protection with password verification failed.
238+
ErrUnprotectWorkbookPassword = errors.New("workbook protect password not match")
233239
)

excelize_test.go

+55
Original file line numberDiff line numberDiff line change
@@ -1329,6 +1329,61 @@ func TestUnprotectSheet(t *testing.T) {
13291329
assert.EqualError(t, f.UnprotectSheet(sheetName, "wrongPassword"), "illegal base64 data at input byte 8")
13301330
}
13311331

1332+
func TestProtectWorkbook(t *testing.T) {
1333+
f := NewFile()
1334+
assert.NoError(t, f.ProtectWorkbook(nil))
1335+
// Test protect workbook with default hash algorithm
1336+
assert.NoError(t, f.ProtectWorkbook(&WorkbookProtectionOptions{
1337+
Password: "password",
1338+
LockStructure: true,
1339+
}))
1340+
wb, err := f.workbookReader()
1341+
assert.NoError(t, err)
1342+
assert.Equal(t, "SHA-512", wb.WorkbookProtection.WorkbookAlgorithmName)
1343+
assert.Equal(t, 24, len(wb.WorkbookProtection.WorkbookSaltValue))
1344+
assert.Equal(t, 88, len(wb.WorkbookProtection.WorkbookHashValue))
1345+
assert.Equal(t, int(workbookProtectionSpinCount), wb.WorkbookProtection.WorkbookSpinCount)
1346+
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestProtectWorkbook.xlsx")))
1347+
// Test protect workbook with password exceeds the limit length
1348+
assert.EqualError(t, f.ProtectWorkbook(&WorkbookProtectionOptions{
1349+
AlgorithmName: "MD4",
1350+
Password: strings.Repeat("s", MaxFieldLength+1),
1351+
}), ErrPasswordLengthInvalid.Error())
1352+
// Test protect workbook with unsupported hash algorithm
1353+
assert.EqualError(t, f.ProtectWorkbook(&WorkbookProtectionOptions{
1354+
AlgorithmName: "RIPEMD-160",
1355+
Password: "password",
1356+
}), ErrUnsupportedHashAlgorithm.Error())
1357+
}
1358+
1359+
func TestUnprotectWorkbook(t *testing.T) {
1360+
f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
1361+
if !assert.NoError(t, err) {
1362+
t.FailNow()
1363+
}
1364+
1365+
assert.NoError(t, f.UnprotectWorkbook())
1366+
assert.EqualError(t, f.UnprotectWorkbook("password"), ErrUnprotectWorkbook.Error())
1367+
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestUnprotectWorkbook.xlsx")))
1368+
assert.NoError(t, f.Close())
1369+
1370+
f = NewFile()
1371+
assert.NoError(t, f.ProtectWorkbook(&WorkbookProtectionOptions{Password: "password"}))
1372+
// Test remove workbook protection with an incorrect password
1373+
assert.EqualError(t, f.UnprotectWorkbook("wrongPassword"), ErrUnprotectWorkbookPassword.Error())
1374+
// Test remove workbook protection with password verification
1375+
assert.NoError(t, f.UnprotectWorkbook("password"))
1376+
// Test with invalid salt value
1377+
assert.NoError(t, f.ProtectWorkbook(&WorkbookProtectionOptions{
1378+
AlgorithmName: "SHA-512",
1379+
Password: "password",
1380+
}))
1381+
wb, err := f.workbookReader()
1382+
assert.NoError(t, err)
1383+
wb.WorkbookProtection.WorkbookSaltValue = "YWJjZA====="
1384+
assert.EqualError(t, f.UnprotectWorkbook("wrongPassword"), "illegal base64 data at input byte 8")
1385+
}
1386+
13321387
func TestSetDefaultTimeStyle(t *testing.T) {
13331388
f := NewFile()
13341389
// Test set default time style on not exists worksheet.

workbook.go

+61
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,67 @@ func (f *File) GetWorkbookProps() (WorkbookPropsOptions, error) {
5959
return opts, err
6060
}
6161

62+
// ProtectWorkbook provides a function to prevent other users from accidentally or
63+
// deliberately changing, moving, or deleting data in a workbook.
64+
func (f *File) ProtectWorkbook(opts *WorkbookProtectionOptions) error {
65+
wb, err := f.workbookReader()
66+
if err != nil {
67+
return err
68+
}
69+
if wb.WorkbookProtection == nil {
70+
wb.WorkbookProtection = new(xlsxWorkbookProtection)
71+
}
72+
if opts == nil {
73+
opts = &WorkbookProtectionOptions{}
74+
}
75+
wb.WorkbookProtection = &xlsxWorkbookProtection{
76+
LockStructure: opts.LockStructure,
77+
LockWindows: opts.LockWindows,
78+
}
79+
if opts.Password != "" {
80+
if opts.AlgorithmName == "" {
81+
opts.AlgorithmName = "SHA-512"
82+
}
83+
hashValue, saltValue, err := genISOPasswdHash(opts.Password, opts.AlgorithmName, "", int(workbookProtectionSpinCount))
84+
if err != nil {
85+
return err
86+
}
87+
wb.WorkbookProtection.WorkbookAlgorithmName = opts.AlgorithmName
88+
wb.WorkbookProtection.WorkbookSaltValue = saltValue
89+
wb.WorkbookProtection.WorkbookHashValue = hashValue
90+
wb.WorkbookProtection.WorkbookSpinCount = int(workbookProtectionSpinCount)
91+
}
92+
return nil
93+
}
94+
95+
// UnprotectWorkbook provides a function to remove protection for workbook,
96+
// specified the second optional password parameter to remove workbook
97+
// protection with password verification.
98+
func (f *File) UnprotectWorkbook(password ...string) error {
99+
wb, err := f.workbookReader()
100+
if err != nil {
101+
return err
102+
}
103+
// password verification
104+
if len(password) > 0 {
105+
if wb.WorkbookProtection == nil {
106+
return ErrUnprotectWorkbook
107+
}
108+
if wb.WorkbookProtection.WorkbookAlgorithmName != "" {
109+
// check with given salt value
110+
hashValue, _, err := genISOPasswdHash(password[0], wb.WorkbookProtection.WorkbookAlgorithmName, wb.WorkbookProtection.WorkbookSaltValue, wb.WorkbookProtection.WorkbookSpinCount)
111+
if err != nil {
112+
return err
113+
}
114+
if wb.WorkbookProtection.WorkbookHashValue != hashValue {
115+
return ErrUnprotectWorkbookPassword
116+
}
117+
}
118+
}
119+
wb.WorkbookProtection = nil
120+
return err
121+
}
122+
62123
// setWorkbook update workbook property of the spreadsheet. Maximum 31
63124
// characters are allowed in sheet title.
64125
func (f *File) setWorkbook(name string, sheetID, rid int) {

xmlWorkbook.go

+8
Original file line numberDiff line numberDiff line change
@@ -320,3 +320,11 @@ type WorkbookPropsOptions struct {
320320
FilterPrivacy *bool `json:"filter_privacy,omitempty"`
321321
CodeName *string `json:"code_name,omitempty"`
322322
}
323+
324+
// WorkbookProtectionOptions directly maps the settings of workbook protection.
325+
type WorkbookProtectionOptions struct {
326+
AlgorithmName string `json:"algorithmName,omitempty"`
327+
Password string `json:"password,omitempty"`
328+
LockStructure bool `json:"lockStructure,omitempty"`
329+
LockWindows bool `json:"lockWindows,omitempty"`
330+
}

0 commit comments

Comments
 (0)