0x01 介紹
在Frp工具中編寫了Socket 對稱加密功能,其主要原理是封裝golang原生io流,使用AES-128-CFB算法將輸入輸出流包裝起來。該部分代碼面向抽象編程,使得無論是文件流還是網絡套接字流都可以使用該方法進行流加密。為了研究學習Frp工具代碼技術,筆者打算分析加解密模塊代碼,並總結相關golang代碼技術。
0x02 Frp加解密相關功能
Frp工具加解密功能使用的是fatedier模塊,該模塊應該是專門為Frp設計,但是其中的設計理念能夠適應很多加解密場景。主要的實現文件如下圖所示
該模塊主要實現了AES-128-CFB加解密算法,而且根據代碼注釋得到,目前並不支持其他加解密算法。
0x03 Frp輸入輸出流加密
要理解這部分知識得先從io.Reader和io.Writer說起。
0x1 io.Writer 和 io.Reader
這兩個類型一樣都是interface類型,他們的功能特別強大,強大到在任何寫入和讀取數據的地方,都應該盡可能的使用這兩個類型對象。首先我們看看他們的聲明代碼
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
只要是實現了這兩個接口中的方法就可以使用該接口實現多態的功能。
0x2 EncryptWriter 設計思路
writer的設計思路可以歸結為以下幾點
- 設計writer結構體,並包含原始套接字(為了發送明文數據)
- 設計writer結構體,並包含加密套接字
- 實現Write函數,發送隨機IV
1. 結構體設計
與其說是結構體設計不如說是在設計類,struct可以有方法,那么有方法的struct在golang中和類的功能也差不多了(而且可以實現接口)
type Writer struct {
w io.Writer
enc *cipher.StreamWriter
key []byte
iv []byte
ivSend bool
err error
}
我們可以看到Writer結構體中幾乎包含了我們能夠想得到的必要字段。
2. 初始化
首先看下代碼
func NewWriter(w io.Writer, key []byte) (*Writer, error) {
key = pbkdf2.Key(key, []byte(DefaultSalt), 64, aes.BlockSize, sha1.New)
// random iv
iv := make([]byte, aes.BlockSize)
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
return nil, err
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
return &Writer{
w: w,
enc: &cipher.StreamWriter{
S: cipher.NewCFBEncrypter(block, iv),
W: w,
},
key: key,
iv: iv,
}, nil
}
(1)生成密鑰
第二行代碼pbkdf2.Key,是一個用來生成密鑰的函數,原理是通過 password 和 salt 進行 hash 加密,然后將結果作為 salt 與 password 再進行 hash,多次重復此過程,生成最終的密文。一般來說采用PBKDF2算法hash密碼進行存儲是比較安全的。pbkdf2.Key函數解析
Key(password, salt []byte, iter, keyLen int, h func() hash.Hash)
- 參數一,明文密碼
- 參數二,鹽值
- 參數三,迭代輪數
- 參數四,最后的hash長度
- 參數五,hash算法
(2)生成iv
rand.Reader是隨機數流,可以從該流中讀取需要長度的隨機數。io.ReadFull函數用於從指定的讀取器r中讀取到指定的緩沖區buf中。
iv := make([]byte, aes.BlockSize)
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
return nil, err
}
(3)創建Writer
block, err := aes.NewCipher(key)
return &Writer{
w: w,
enc: &cipher.StreamWriter{
S: cipher.NewCFBEncrypter(block, iv),
W: w,
},
key: key,
iv: iv,
}, nil
最后成功創建Writer類,而且包含了加密通信所必要的幾個元素
3. 實現Write函數
Write函數主要是為了協商iv以及發送加密數據
func (w *Writer) Write(p []byte) (nRet int, errRet error) {
if w.err != nil {
return 0, w.err
}
// When write is first called, iv will be written to w.w
if !w.ivSend {
w.ivSend = true
_, errRet = w.w.Write(w.iv)
if errRet != nil {
w.err = errRet
return
}
}
nRet, errRet = w.enc.Write(p)
if errRet != nil {
w.err = errRet
}
return
}
設置了ivSend標志字段,如果是第一次通信,將會使用原生套接字發送十六字節iv值。之后使用w.enc套接字發送通信內容。
0x3 DecryptReader 設計思路
reader的設計思路可以歸結為以下幾點
- 設計reader結構體,並包含原始套接字(為了接收明文數據)
- 設計reader結構體,並包含加密套接字
- 實現Read函數,接受隨機IV
1.結構體設計
type Reader struct {
r io.Reader
dec *cipher.StreamReader
key []byte
iv []byte
err error
}
2.初始化
Reader的初始化相對來說比較簡單,因為密鑰是根據token計算得來的,iv需要接受來自套接字另一端的writer。該部分的重點代碼在Read函數的實現上。
(1)生成密鑰
采用和writer端相同的密鑰生成方式,保證在token相同的情況下aes key也是一樣的
key = pbkdf2.Key(key, []byte(DefaultSalt), 64, aes.BlockSize, sha1.New)
(2)創建Reader
因為Reader在創建的時候知道的字段有限,所以創建時只需指定已知字段即可,創建方式如下
return &Reader{
r: r,
key: key,
}
主要的參數通過和writer端協商之后進行填充,詳情可參照Read函數的實現
3.實現Read函數
func (r *Reader) Read(p []byte) (nRet int, errRet error) {
if r.err != nil {
return 0, r.err
}
if r.dec == nil {
iv := make([]byte, aes.BlockSize)
if _, errRet = io.ReadFull(r.r, iv); errRet != nil {
return//從套接字另一端讀取iv值
}
r.iv = iv
block, err := aes.NewCipher(r.key)
if err != nil {
errRet = err
return
}
r.dec = &cipher.StreamReader{//創建加密流
S: cipher.NewCFBDecrypter(block, iv),
R: r.r,
}
}
nRet, errRet = r.dec.Read(p)//進行數據解密,並讀到緩沖區
if errRet != nil {
r.err = errRet
}
return
}
0x03 加解密代碼優化
讀了這部分的代碼總感覺有點怪怪的,整體的感覺就是client端有read和write,server端有read和write,但他們的iv是兩兩配對的,比如client的read和server端的write是一個iv密鑰對。明明是一個網絡流還整出了兩套不一樣的iv向量,着實有些奇怪。為了優化該部分代碼,筆者將兩個結構體抽象成了一個結構體,來完成所有的操作,並增加協商方法,在套接字建立連接之后開始進行密鑰協商。
0x1 結構體設計
type SecSocket struct {
w io.Writer
r io.Reader
iv []byte
key []byte
enc *cipher.StreamWriter
dec *cipher.StreamReader
isNego bool
err error
}
設計該結構體的初衷就是將iv及key綁定在同一套接字兩端的輸入輸出流加解密算法上。因此在SecSocket包含了io.Writer和io.Reader兩個接口,其初始化函數如下所示
func NewSecSocket(r io.Reader,w io.Writer)(*SecSocket, error){
return &SecSocket{
w: w,
r: r,
isNego: false,
err: nil,
},nil
}
0x2 密鑰協商
和Frp中的設計理念不同的是SecSocket需要一個密鑰協商過程,協商的時候綁定iv和key值。先發起連接請求的一端為NegotiateC協商函數,負責監聽的一端為NegotiateS協商函數。
func (ss *SecSocket) NegotiateC()(errRet error) {
iv := make([]byte, aes.BlockSize)
if _, err := io.ReadFull(rand.Reader, iv); err != nil {//隨機生成iv
return err
}
ss.iv = iv
key := make([]byte, aes.BlockSize)
if _, err := io.ReadFull(rand.Reader, key); err != nil {//隨機生成key
return err
}
ss.key = key
_,errRet = ss.w.Write(key)//使用原生socket發送key
if errRet != nil {
ss.err = errRet
return
}
_,errRet = ss.w.Write(iv)//使用原生socket發送iv
if errRet != nil {
ss.err = errRet
return
}
block, err := aes.NewCipher(ss.key)//初始化算法
if err != nil {
errRet = err
return
}
ss.dec = &cipher.StreamReader{
S: cipher.NewCFBDecrypter(block, ss.iv),//生成解密算法
R: ss.r,
}
ss.enc = &cipher.StreamWriter{
S: cipher.NewCFBEncrypter(block, ss.iv),//生成加密算法
W: ss.w,
}
ss.isNego = true
return nil
}
整體來說client端在密鑰協商過程中只負責發送iv和key,和frp的協商還不太一樣的地方在於key也是隨機生成的並參與協商過程,這樣每次建立通信連接使用的密鑰也是不一樣的。最后給dec和enc加密流選擇算法並賦值。
反過來看下服務端,邏輯更是簡單,接受來自對端的iv和key並給dec和enc加密流選擇算法並賦值。
func (ss *SecSocket) NegotiateS()(errRet error){
key := make([]byte, aes.BlockSize)
if _, errRet = io.ReadFull(ss.r, key); errRet != nil {//接收key
return
}
ss.key = key
iv := make([]byte, aes.BlockSize)
if _, errRet = io.ReadFull(ss.r, iv); errRet != nil {//接收iv
return
}
ss.iv = iv
block, err := aes.NewCipher(ss.key)
if err != nil {
errRet = err
return
}
ss.dec = &cipher.StreamReader{
S: cipher.NewCFBDecrypter(block, ss.iv),//生成解密算法
R: ss.r,
}
ss.enc = &cipher.StreamWriter{
S: cipher.NewCFBEncrypter(block, ss.iv),//生成加密算法
W: ss.w,
}
ss.isNego = true
return
}
0x3 寫入讀取函數
將p緩沖區寫入enc加密流
func (ss *SecSocket) Write(p []byte) (nRet int, errRet error) {
if !ss.isNego {
}
nRet, errRet = ss.enc.Write(p)
if errRet != nil {
ss.err = errRet
}
return
}
從dec解密流中讀取字符到p緩沖區
func (ss *SecSocket) Read(p []byte) (nRet int, errRet error) {
if ss.err != nil {
return 0, ss.err
}
nRet, errRet = ss.dec.Read(p)
if errRet != nil {
ss.err = errRet
}
return
}
0x04 擴展功能設計
筆者想基於Frp的加解密功能實現一些擴展功能,列表如下: