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/