golang 解壓帶密碼的zip包,同時支持指定文件頭偏移量加載zip包。下面首先給出完整的代碼,后面再對代碼實現過程的思考和原理做詳細解釋。
package main
import (
"archive/zip"
"bytes"
"compress/flate"
"fmt"
"hash/crc32"
"io"
"io/ioutil"
"os"
"path/filepath"
)
type ZipCrypto struct {
password []byte
Keys [3]uint32
}
func NewZipCrypto(passphrase []byte) *ZipCrypto {
z := &ZipCrypto{}
z.password = passphrase
z.init()
return z
}
func (z *ZipCrypto) init() {
z.Keys[0] = 0x12345678
z.Keys[1] = 0x23456789
z.Keys[2] = 0x34567890
for i := 0; i < len(z.password); i++ {
z.updateKeys(z.password[i])
}
}
func (z *ZipCrypto) updateKeys(byteValue byte) {
z.Keys[0] = crc32update(z.Keys[0], byteValue)
z.Keys[1] += z.Keys[0] & 0xff
z.Keys[1] = z.Keys[1]*134775813 + 1
z.Keys[2] = crc32update(z.Keys[2], (byte)(z.Keys[1]>>24))
}
func (z *ZipCrypto) magicByte() byte {
var t uint32 = z.Keys[2] | 2
return byte((t * (t ^ 1)) >> 8)
}
func (z *ZipCrypto) Encrypt(data []byte) []byte {
length := len(data)
chiper := make([]byte, length)
for i := 0; i < length; i++ {
v := data[i]
chiper[i] = v ^ z.magicByte()
z.updateKeys(v)
}
return chiper
}
func (z *ZipCrypto) Decrypt(chiper []byte) []byte {
length := len(chiper)
plain := make([]byte, length)
for i, c := range chiper {
v := c ^ z.magicByte()
z.updateKeys(v)
plain[i] = v
}
return plain
}
func crc32update(pCrc32 uint32, bval byte) uint32 {
return crc32.IEEETable[(pCrc32^uint32(bval))&0xff] ^ (pCrc32 >> 8)
}
func ZipCryptoDecryptor(r *io.SectionReader, password []byte) (*io.SectionReader, error) {
z := NewZipCrypto(password)
b := make([]byte, r.Size())
r.Read(b)
m := z.Decrypt(b)
return io.NewSectionReader(bytes.NewReader(m), 12, int64(len(m))), nil
}
type unzip struct {
offset int64
fp *os.File
name string
}
func (uz *unzip) init() (err error) {
uz.fp, err = os.Open(uz.name)
return err
}
func (uz *unzip) close() {
if uz.fp != nil {
uz.fp.Close()
}
}
func (uz *unzip) Size() int64 {
if uz.fp == nil {
if err := uz.init(); err != nil {
return -1
}
}
fi, err := uz.fp.Stat()
if err != nil {
return -1
}
return fi.Size() - uz.offset
}
func (uz *unzip) ReadAt(p []byte, off int64) (int, error) {
if uz.fp == nil {
if err := uz.init(); err != nil {
return 0, err
}
}
return uz.fp.ReadAt(p, off+uz.offset)
}
func isInclude(includes []string, fname string) bool {
if includes == nil {
return true
}
for i := 0; i < len(includes); i++ {
if includes[i] == fname {
return true
}
}
return false
}
//DeCompressZip 解壓zip包
func DeCompressZip(zipFile, dest, passwd string, includes []string, offset int64) error {
uz := &unzip{offset: offset, name: zipFile}
defer uz.close()
zr, err := zip.NewReader(uz, uz.Size())
if err != nil {
return err
}
if passwd != "" {
// Register a custom Deflate compressor.
zr.RegisterDecompressor(zip.Deflate, func(r io.Reader) io.ReadCloser {
rs := r.(*io.SectionReader)
r, _ = ZipCryptoDecryptor(rs, []byte(passwd))
return flate.NewReader(r)
})
zr.RegisterDecompressor(zip.Store, func(r io.Reader) io.ReadCloser {
rs := r.(*io.SectionReader)
r, _ = ZipCryptoDecryptor(rs, []byte(passwd))
return ioutil.NopCloser(r)
})
}
for _, f := range zr.File {
fpath := filepath.Join(dest, f.Name)
if f.FileInfo().IsDir() {
os.MkdirAll(fpath, os.ModePerm)
continue
}
if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
return err
}
inFile, err := f.Open()
if err != nil {
return err
}
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil {
inFile.Close()
return err
}
_, err = io.Copy(outFile, inFile)
inFile.Close()
outFile.Close()
if err != nil {
return err
}
}
return nil
}
func main() {
err := DeCompressZip("test.zip", "./tmp", "password", nil, 0)
if err != nil {
fmt.Println(err)
}
return
}
下面給出思考過程
golang zip包的解壓有官方的zip包(archive/zip),但是官方給的zip解壓包代碼只有解壓不帶密碼的zip包,如果我們要解壓帶密碼的zip就做不了了。這時候我們不要急着去尋找第三方的庫去使用,我們先從設計者的角度思考,這是一個官方的代碼庫,zip帶密碼解壓和壓縮是很常見的功能,官方的代碼應該是要支持的,如果沒有封裝好的接口那就看一下是否有預留一些注冊接口讓我們自己去實現解壓的代碼。
看一下golang官方的zip代碼庫,https://golang.google.cn/pkg/archive/zip/#pkg-examples,,我們很容易看到有兩個注冊接口,一個是壓縮和一個是解壓的

既然有注冊接口,那接下來我們需要做的就是研究怎么使用這個接口,還有怎么寫解壓算法。
怎么使用注冊解壓的接口官方沒有給出例子,不過使用注冊壓縮算法,兩個接口差不過是可以參考一下:
package main
import (
"archive/zip"
"bytes"
"compress/flate"
"io"
)
func main() {
// Override the default Deflate compressor with a higher compression level.
// Create a buffer to write our archive to.
buf := new(bytes.Buffer)
// Create a new zip archive.
w := zip.NewWriter(buf)
// Register a custom Deflate compressor.
w.RegisterCompressor(zip.Deflate, func(out io.Writer) (io.WriteCloser, error) {
return flate.NewWriter(out, flate.BestCompression)
})
// Proceed to add files to w.
}
通過官方的例子還有源碼解壓和壓縮的源碼我們可以寫出這樣子的解壓代碼:
zr, err := zip.NewReader(uz, uz.Size())
if err != nil {
return err
}
zr.RegisterDecompressor(zip.Deflate, func(r io.Reader) io.ReadCloser { rs := r.(*io.SectionReader) r, _ = ZipCryptoDecryptor(rs, []byte(passwd)) //這里是解壓算法核心代碼,對rs數據流的代碼進行解密 return flate.NewReader(r) })
編寫完注冊解壓代碼,現在需要實現解密算法,標准的zip解密算法網上有公開的算法,這種沒啥好思考的,按照標准的zip加解密算法來就可以了,這里我參考的是一個開源庫的代碼:
https://github.com/yeka/zip,關於這個庫我這里講一下:我們可以直接用里的代碼,我們需要的接口里面都已經實現好了,里面的整個代碼是在官方的zip庫的基礎上做了一些修改,改的比較多,我們不需要這么多,項目中也不想引入第三方庫就只引用了他里面寫的zip解密代碼。另外還有一點我不是很喜歡的是,這個庫里面的代碼拋棄了注冊的功能,直接把解密代碼寫在了open文件里,廢棄了一個很好用的功能。
加解密的代碼如下:
type ZipCrypto struct {
password []byte
Keys [3]uint32
}
func NewZipCrypto(passphrase []byte) *ZipCrypto {
z := &ZipCrypto{}
z.password = passphrase
z.init()
return z
}
func (z *ZipCrypto) init() {
z.Keys[0] = 0x12345678
z.Keys[1] = 0x23456789
z.Keys[2] = 0x34567890
for i := 0; i < len(z.password); i++ {
z.updateKeys(z.password[i])
}
}
func (z *ZipCrypto) updateKeys(byteValue byte) {
z.Keys[0] = crc32update(z.Keys[0], byteValue)
z.Keys[1] += z.Keys[0] & 0xff
z.Keys[1] = z.Keys[1]*134775813 + 1
z.Keys[2] = crc32update(z.Keys[2], (byte)(z.Keys[1]>>24))
}
func (z *ZipCrypto) magicByte() byte {
var t uint32 = z.Keys[2] | 2
return byte((t * (t ^ 1)) >> 8)
}
func (z *ZipCrypto) Encrypt(data []byte) []byte {
length := len(data)
chiper := make([]byte, length)
for i := 0; i < length; i++ {
v := data[i]
chiper[i] = v ^ z.magicByte()
z.updateKeys(v)
}
return chiper
}
func (z *ZipCrypto) Decrypt(chiper []byte) []byte {
length := len(chiper)
plain := make([]byte, length)
for i, c := range chiper {
v := c ^ z.magicByte()
z.updateKeys(v)
plain[i] = v
}
return plain
}
func crc32update(pCrc32 uint32, bval byte) uint32 {
return crc32.IEEETable[(pCrc32^uint32(bval))&0xff] ^ (pCrc32 >> 8)
}
func ZipCryptoDecryptor(r *io.SectionReader, password []byte) (*io.SectionReader, error) {
z := NewZipCrypto(password)
b := make([]byte, r.Size())
r.Read(b)
m := z.Decrypt(b)
return io.NewSectionReader(bytes.NewReader(m), 12, int64(len(m))), nil
}
這樣子基本就實現了對有密碼的zip做解壓。
不過有時候我們可能會在zip包頭部加上一些信息,這樣子的 zip直接用zip.NewReader打開 是會被識別未 非法zip的,這時候我們讀取文件時就需要設置一個頭部偏移量。設置頭部偏移量這里有一個坑,為我們不能直接用Seek去設置,這樣子在zip.NewReader里是沒有效果的,我們需要再實現一個ReadAt函數
type unzip struct {
offset int64
fp *os.File
name string
}
func (uz *unzip) ReadAt(p []byte, off int64) (int, error) {
if uz.fp == nil {
if err := uz.init(); err != nil {
return 0, err
}
}
return uz.fp.ReadAt(p, off+uz.offset)
}
最后用的地方是:uz里有ReadAt的方法
uz := &unzip{offset: offset, name: zipFile}
defer uz.close()
zr, err := zip.NewReader(uz, uz.Size())
if err != nil {
return err
}
以上均實現后就可以實現一個支持頭部偏移和帶密碼解壓zip包的代碼
參考資料:
https://golang.google.cn/pkg/archive/zip/
