引子

对文件进行加密和解密。

对文件进行压缩处理。

要求和技术选型

界面化操作,独立文件无依赖包,尽可能小。

界面,选择window环境下的最小桌面化,http://github.com/lxn/walk。

加解密算法:选择golang的crypto包。

压缩算法:选择golang的archive/zip。

文件大小:golang编译后文件较大,选用upx进行文件压缩。

实现

界面实现

拖拽文件到程序进行加密和解密。

界面代码

package main

import (
	"filepacker/encrypt"
	"os"
	"path/filepath"

	"github.com/lxn/walk"
	. "github.com/lxn/walk/declarative"
)

type FileInfo struct {
	Name string
	Size int64
}

type FileInfoModel struct {
	walk.SortedReflectTableModelBase
	dirPath string
	items   []*FileInfo
}

func (m *FileInfoModel) Items() interface{} {
	return m.items
}

func NewFileInfoModel() *FileInfoModel {
	return new(FileInfoModel)
}

func main() {

	var mw *walk.MainWindow
	var pwdLE *walk.LineEdit
	var checkBoxZip *walk.CheckBox
	var encryptPB, decryptPB *walk.PushButton
	var tableView *walk.TableView
	tableModel := NewFileInfoModel()

	MainWindow{
		AssignTo: &mw,
		Title:    "Filepacker 文件加解密工具",
		MinSize:  Size{320, 240},
		Size:     Size{480, 320},
		Layout:   VBox{},
		OnDropFiles: func(files []string) {
			for _, f := range files {

				info, _ := os.Stat(f)
				if info.IsDir() {
					continue
				}

				item := &FileInfo{
					Name: f,
					Size: info.Size(),
				}

				tableModel.items = append(tableModel.items, item)
			}
			tableModel.PublishRowsReset()
		},
		Children: []Widget{
			TableView{
				AssignTo:      &tableView,
				StretchFactor: 2,
				Columns: []TableViewColumn{
					TableViewColumn{
						DataMember: "Name",
						Title:      "文件名",
						Width:      192,
					},
					TableViewColumn{
						DataMember: "Size",
						Title:      "大小",
						Format:     "%d",
						Alignment:  AlignFar,
						Width:      64,
					},
				},
				Model: tableModel,
				OnCurrentIndexChanged: func() {

				},
			},
			Composite{
				Layout: HBox{},
				Children: []Widget{
					Label{
						Text: "密码:",
					},
					LineEdit{
						AssignTo: &pwdLE,
						Text:     "1234567890abcdef",
					},
					LinkLabel{
						MaxSize: Size{100, 0},
						Text:    `<a id="this" href="#">随机密码</a>`,
						OnLinkActivated: func(link *walk.LinkLabelLink) {

							uuid := encrypt.NewUUID()
							pwdLE.SetText(uuid.String())
						},
					},
				},
			},
			Composite{
				Layout: HBox{},
				Children: []Widget{
					CheckBox{
						AssignTo: &checkBoxZip,
						Name:     "doZip",
						Text:     "压缩",
						Checked:  true,
					},

					PushButton{
						AssignTo: &encryptPB,

						Text: "加密",
						OnClicked: func() {
							pwd := pwdLE.Text()
							if pwd == "" || len(pwd) < 8 {
								walk.MsgBox(
									mw,
									"Error",
									"请输入足够长度的密码",
									walk.MsgBoxOK|walk.MsgBoxIconError)
								return
							}

							for _, f := range tableModel.items {

								src := f.Name
								dir, fname := filepath.Split(src)
								des := filepath.Join(dir, "aes"+fname)

								err := encrypt.EncryptFile(src, des, pwd)

								if checkBoxZip.Checked() {
									err := encrypt.Zip(des, des+".zip")
									if err == nil {
										os.RemoveAll(des)
									}
								}

								if err == nil {
									walk.MsgBox(
										mw,
										"信息",
										"加密成功:"+des,
										walk.MsgBoxOK|walk.MsgBoxIconInformation)
								} else {

									walk.MsgBox(
										mw,
										"Error",
										err.Error(),
										walk.MsgBoxOK|walk.MsgBoxIconError)
								}
							}

							tableModel.items = tableModel.items[0:0]
							tableModel.PublishRowsReset()
						},
					},
					PushButton{
						AssignTo: &decryptPB,
						Text:     "解密",
						OnClicked: func() {

							pwd := pwdLE.Text()
							if pwd == "" || len(pwd) < 8 {
								walk.MsgBox(
									mw,
									"Error",
									"请输入足够长度的密码",
									walk.MsgBoxOK|walk.MsgBoxIconError)
								return
							}

							for _, f := range tableModel.items {

								src := f.Name

								dir, fname := filepath.Split(src)

								des := filepath.Join(dir, "des"+fname)

								err := encrypt.DecryptFile(src, des, pwd)
								if err == nil {
									walk.MsgBox(
										mw,
										"信息",
										"解密成功:"+des,
										walk.MsgBoxOK|walk.MsgBoxIconInformation)
								} else {

									walk.MsgBox(
										mw,
										"Error",
										err.Error(),
										walk.MsgBoxOK|walk.MsgBoxIconError)
								}
							}
							tableModel.items = tableModel.items[0:0]
							tableModel.PublishRowsReset()
						},
					},
				},
			},
		},
	}.Run()
}

加解密代码

package encrypt

import (
	"bytes"
	"crypto/aes"
	"crypto/cipher"
	"encoding/binary"
	"errors"
	"io"
	"os"
	"strings"
)

const (
	FrameSize = 2048

	FPFileVersion1 uint16 = 0xfe
	FPFileVersion2 uint16 = 0xef
	FPFileVersion3 uint8  = 1

	FDInfo string = "蒹葭苍苍,白露为霜。所谓伊人,在水一方。"
)

//只能16、24、32共三种情况
func pwdPadding(pwd []byte) []byte {
	maxSize := 32
	pwdSize := len(pwd)
	if pwdSize == 32 {
		return pwd
	} else if pwdSize > 32 {
		return pwd[0:32]
	} else {
		padding := maxSize - pwdSize
		padtext := bytes.Repeat([]byte{byte(padding)}, padding)
		return append(pwd, padtext...)
	}
	// if pwdSize%minSize == 0 {
	// 	return pwd
	// } else if pwdSize <= minSize {
	// 	padding := minSize - pwdSize
	// 	padtext := bytes.Repeat([]byte{byte(padding)}, padding)
	// 	return append(pwd, padtext...)
	// } else {
	// 	padding := minSize - pwdSize%minSize
	// 	padtext := bytes.Repeat([]byte{byte(padding)}, padding)
	// 	return append(pwd, padtext...)
	// }
}

func EncryptFile(src, dest, pwd string) error {

	correctPwd := pwdPadding([]byte(pwd))

	srcfile, err := os.Open(src)
	if err != nil {
		return err
	}
	defer srcfile.Close()

	destfile, err := os.Create(dest)
	if err != nil {
		return err
	}
	defer destfile.Close()

	//写文件头
	var encryptedInfo = AesEncryptCBC([]byte(FDInfo), correctPwd)
	var fdHeader []byte = make([]byte, 6)
	binary.BigEndian.PutUint16(fdHeader[0:], FPFileVersion1)
	binary.BigEndian.PutUint16(fdHeader[2:], FPFileVersion2)
	binary.BigEndian.PutUint16(fdHeader[4:], uint16(len(encryptedInfo)))
	destfile.Write(fdHeader)
	destfile.Write(encryptedInfo)

	//加密处理
	frameBuf := make([]byte, FrameSize) //一次读取多少个字节
	sizeBuf := make([]byte, 2)
	for {
		n, err := srcfile.Read(frameBuf)
		if err != nil { //遇到任何错误立即返回,并忽略 EOF 错误信息
			if err == io.EOF {
				break
			}
			return err
		}
		if n <= 0 {
			break
		}

		encryptedFrame := AesEncryptCBC(frameBuf[:n], correctPwd)
		binary.BigEndian.PutUint16(sizeBuf[:], uint16(len(encryptedFrame)))
		_, err = destfile.Write(sizeBuf)
		if err != nil {
			return err
		}
		_, err = destfile.Write(encryptedFrame)
		if err != nil {
			return err
		}
	}

	return nil
}

func DecryptFile(src, dest, pwd string) error {
	correctPwd := pwdPadding([]byte(pwd))

	srcfile, err := os.Open(src)
	if err != nil {
		return err
	}
	defer srcfile.Close()

	//读文件头
	fdHeader := make([]byte, 6)
	srcfile.Read(fdHeader)

	v1 := binary.BigEndian.Uint16(fdHeader[0:])
	v2 := binary.BigEndian.Uint16(fdHeader[2:])
	encryptedInfoSize := binary.BigEndian.Uint16(fdHeader[4:])

	if v1 != FPFileVersion1 && v2 != FPFileVersion2 {
		return errors.New("Not a FP file")
	}
	encryptedInfo := make([]byte, encryptedInfoSize)
	srcfile.Read(encryptedInfo)

	descryptedFDInfo := AesEncryptCBC([]byte(FDInfo), correctPwd)

	if strings.Compare(string(descryptedFDInfo), string(encryptedInfo)) != 0 {
		return errors.New("password wrong")
	}

	sizeBuf := make([]byte, 2)
	destfile, err := os.Create(dest)
	if err != nil {
		return err
	}
	defer destfile.Close()
	for {
		n, err := srcfile.Read(sizeBuf)
		if err != nil { //遇到任何错误立即返回,并忽略 EOF 错误信息
			if err == io.EOF {
				break
			}
			return err
		}
		if n <= 0 {
			break
		}
		encryptedFrameSize := binary.BigEndian.Uint16(sizeBuf)
		buf := make([]byte, encryptedFrameSize)
		n, err = srcfile.Read(buf)
		if err != nil { //遇到任何错误立即返回,并忽略 EOF 错误信息
			if err == io.EOF {
				break
			}
			return err
		}

		if n <= 0 {
			break
		}

		d := AesDecryptCBC(buf[:n], correctPwd)
		destfile.Write(d)
	}

	return nil
}

// =================== CBC ======================
func AesEncryptCBC(origData []byte, key []byte) (encrypted []byte) {
	// 分组秘钥
	// NewCipher该函数限制了输入k的长度必须为16, 24或者32
	block, _ := aes.NewCipher(key)
	blockSize := block.BlockSize()                              // 获取秘钥块的长度
	origData = pkcs5Padding(origData, blockSize)                // 补全码
	blockMode := cipher.NewCBCEncrypter(block, key[:blockSize]) // 加密模式
	encrypted = make([]byte, len(origData))                     // 创建数组
	blockMode.CryptBlocks(encrypted, origData)                  // 加密
	return encrypted
}

func AesDecryptCBC(encrypted []byte, key []byte) (decrypted []byte) {
	block, _ := aes.NewCipher(key)                              // 分组秘钥
	blockSize := block.BlockSize()                              // 获取秘钥块的长度
	blockMode := cipher.NewCBCDecrypter(block, key[:blockSize]) // 加密模式
	decrypted = make([]byte, len(encrypted))                    // 创建数组
	blockMode.CryptBlocks(decrypted, encrypted)                 // 解密
	decrypted = pkcs5UnPadding(decrypted)                       // 去除补全码
	return decrypted
}

func pkcs5Padding(ciphertext []byte, blockSize int) []byte {
	padding := blockSize - len(ciphertext)%blockSize
	padtext := bytes.Repeat([]byte{byte(padding)}, padding)
	return append(ciphertext, padtext...)
}

func pkcs5UnPadding(origData []byte) []byte {
	length := len(origData)
	unpadding := int(origData[length-1])
	return origData[:(length - unpadding)]
}

// =================== ECB ======================
func AesEncryptECB(origData []byte, key []byte) (encrypted []byte) {
	newCipher, _ := aes.NewCipher(generateKey(key))
	length := (len(origData) + aes.BlockSize) / aes.BlockSize
	plain := make([]byte, length*aes.BlockSize)
	copy(plain, origData)
	pad := byte(len(plain) - len(origData))
	for i := len(origData); i < len(plain); i++ {
		plain[i] = pad
	}
	encrypted = make([]byte, len(plain))
	// 分组分块加密
	for bs, be := 0, newCipher.BlockSize(); bs <= len(origData); bs, be = bs+newCipher.BlockSize(), be+newCipher.BlockSize() {
		newCipher.Encrypt(encrypted[bs:be], plain[bs:be])
	}
	return encrypted
}

func AesDecryptECB(encrypted []byte, key []byte) (decrypted []byte) {
	newCipher, _ := aes.NewCipher(generateKey(key))
	decrypted = make([]byte, len(encrypted))

	for bs, be := 0, newCipher.BlockSize(); bs < len(encrypted); bs, be = bs+newCipher.BlockSize(), be+newCipher.BlockSize() {
		newCipher.Decrypt(decrypted[bs:be], encrypted[bs:be])
	}

	trim := 0
	if len(decrypted) > 0 {
		trim = len(decrypted) - int(decrypted[len(decrypted)-1])
	}
	return decrypted[:trim]
}

func generateKey(key []byte) (genKey []byte) {
	genKey = make([]byte, 16)
	copy(genKey, key)
	for i := 16; i < len(key); {
		for j := 0; j < 16 && i < len(key); j, i = j+1, i+1 {
			genKey[j] ^= key[i]
		}
	}
	return genKey
}

gowalk准备syso文件

准备manifest文件

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
	<assemblyIdentity version="1.0.0.0" processorArchitecture="*" name="SomeFunkyNameHere" type="win32"/>
	<dependency>
		<dependentAssembly>
			<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
		</dependentAssembly>
	</dependency>
	<asmv3:application>
		<asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
			<dpiAware>true</dpiAware>
		</asmv3:windowsSettings>
	</asmv3:application>
</assembly>

执行命令:

go get github.com/akavel/rsrc
rsrc -manifest filepacker.exe.manifest -o rsrc.syso

编译文件大小处理

通过指定go的编译参数,缩减编译后文件大小。

go build -ldflags="-s -w -H windowsgui"

通过upx进行文件压缩。

upx filepacker.exe

环境清单

GoLang1.17.6 windows/amd64 The Go Programming Language

制成文件

filepacker.exe
1.7M
·
百度网盘

遗留问题

golang编译出的文件目前最小压缩到1.7m,目前没有找到更好的解决方式进一步减小文件尺寸,网上有说可通过go的编译过程中控制引用的包控制尺寸,感觉比较麻烦,没有实际验证。

大神提出增加自解压功能方便使用,考虑到golang编译后的文件较大,准备选用vc++进行实现。