Golang如何在32位机器上使用AEAD模式加密2GB文件

Golang如何在32位机器上使用AEAD模式加密2GB文件 根据NIST SP 800-38D (GCM) 第5.2.1.1节,明文的最大长度似乎是2^39-256位,约等于64 GB。但是,对于任何AEAD操作模式,大于1GB的文件都会导致内存错误,无法加密1GB的文件:

package main
import (
	"crypto/cipher"
	"crypto/rand"
	"crypto/aes"
	"bytes"
	"encoding/hex"
	"flag"
	"fmt"
	"crypto/sha256"
	"golang.org/x/crypto/pbkdf2"
	"io"
	"log"
	"os"
)

	var dec = flag.Bool("d", false, "Decrypt instead Encrypt.")
	var iter = flag.Int("i", 1024, "Iterations. (for PBKDF2)")
	var key = flag.String("k", "", "128-bit key to Encrypt/Decrypt.")
	var pbkdf = flag.String("p", "", "PBKDF2.")
	var salt = flag.String("s", "", "Salt. (for PBKDF2)")

func main() {
    flag.Parse()

        if (len(os.Args) < 1) {
	fmt.Println("Usage of",os.Args[0]+":")
        flag.PrintDefaults()
        os.Exit(1)
        }
	
	var keyHex string
	var prvRaw []byte
	if *pbkdf != "" {
	prvRaw = pbkdf2.Key([]byte(*pbkdf), []byte(*salt), *iter, 32, sha256.New)
	keyHex = hex.EncodeToString(prvRaw)
	} else {
	keyHex = *key
	}
	var key []byte
	var err error
	if keyHex == "" {
		key = make([]byte, 32)
		_, err = io.ReadFull(rand.Reader, key)
		if err != nil {
                        log.Fatal(err)
		}
		fmt.Fprintln(os.Stderr, "Key=", hex.EncodeToString(key))
	} else {
		key, err = hex.DecodeString(keyHex)
		if err != nil {
                        log.Fatal(err)
		}
		if len(key) != 32 {
                        log.Fatal(err)
		}
	}

	block, err := aes.NewCipher(key)
	if err != nil {
		panic(err.Error())
	}

	aead, err := cipher.NewGCM(block)
	if err != nil {
		panic(err.Error())
	}

	if *dec == false {
		buf := bytes.NewBuffer(nil)
		data := os.Stdin 
		io.Copy(buf, data)
		msg := buf.Bytes()

		nonce := make([]byte, aead.NonceSize(), aead.NonceSize()+len(msg)+aead.Overhead())

		out := aead.Seal(nonce, nonce, msg, nil)
		fmt.Printf("%s", out)

	        os.Exit(0)
	}

	if *dec == true {
		buf := bytes.NewBuffer(nil)
		data := os.Stdin 
		io.Copy(buf, data)
		msg := buf.Bytes()

		nonce, msg := msg[:aead.NonceSize()], msg[aead.NonceSize():]

		out, err := aead.Open(nil, nonce, msg, nil)
		if err != nil {
			panic(err)
		}
		fmt.Printf("%s", out)

	        os.Exit(0)
	}
}

runtime: 内存不足:无法分配1073741824字节的块(已使用1077837824字节) 致命错误:内存不足

runtime stack: runtime.throw(0x4d9c26, 0xd) c:/go/src/runtime/panic.go:617 +0x64 runtime.largeAlloc(0x3ffffe00, 0x11c10101, 0x11c1e000) c:/go/src/runtime/malloc.go:1057 +0x10f runtime.mallocgc.func1() c:/go/src/runtime/malloc.go:950 +0x39 runtime.systemstack(0x447245) c:/go/src/runtime/asm_386.s:396 +0x53 runtime.mstart() c:/go/src/runtime/proc.go:1153

goroutine 1 [running]: runtime.systemstack_switch() c:/go/src/runtime/asm_386.s:357 fp=0x11c56d94 sp=0x11c56d90 pc=0x447300 runtime.mallocgc(0x3ffffe00, 0x4c0340, 0x1, 0x11c56e00) c:/go/src/runtime/malloc.go:949 +0x65b fp=0x11c56de8 sp=0x11c56d94 pc=0x40968b runtime.makeslice(0x4c0340, 0x3ffffe00, 0x3ffffe00, 0x527ffe00) c:/go/src/runtime/slice.go:49 +0x4f fp=0x11c56dfc sp=0x11c56de8 pc=0x43521f bytes.makeSlice(0x3ffffe00, 0x0, 0x0, 0x0) c:/go/src/bytes/buffer.go:232 +0x61 fp=0x11c56e10 sp=0x11c56dfc pc=0x491821 bytes.(*Buffer).grow(0x11c3bb60, 0x200, 0x10000000) c:/go/src/bytes/buffer.go:145 +0x12a fp=0x11c56e38 sp=0x11c56e10 pc=0x4913ca bytes.(*Buffer).ReadFrom(0x11c3bb60, 0x4f5920, 0x11c380d8, 0x3880e0, 0x11c3bb60, 0x1, 0x75) c:/go/src/bytes/buffer.go:205 +0x45 fp=0x11c56e74 sp=0x11c56e38 pc=0x491645 io.copyBuffer(0x4f5880, 0x11c3bb60, 0x4f5920, 0x11c380d8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4d8408, …) c:/go/src/io/io.go:388 +0x29a fp=0x11c56eb4 sp=0x11c56e74 pc=0x44e65a io.Copy(…) c:/go/src/io/io.go:364 main.main() H:/PGMM/crypter/aes-gcm/main.go:93 +0x5c6 fp=0x11c56fd0 sp=0x11c56eb4 pc=0x4a9366 runtime.main() c:/go/src/runtime/proc.go:200 +0x1d7 fp=0x11c56ff0 sp=0x11c56fd0 pc=0x427937 runtime.goexit() c:/go/src/runtime/asm_386.s:1321 +0x1 fp=0x11c56ff4 sp=0x11c56ff0 pc=0x448b21

该如何进行?

提前感谢!


更多关于Golang如何在32位机器上使用AEAD模式加密2GB文件的实战教程也可以访问 https://www.itying.com/category-94-b0.html

17 回复

怎么操作?

更多关于Golang如何在32位机器上使用AEAD模式加密2GB文件的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


将文件像切蛋糕一样分成多块,对每一块施以魔法,然后再将它们漂亮地打包回一个单独的文件?

(1) 你有一个大蛋糕(>1GB) (2) 根据可食用大小进行切片(约500MB?) (3) 对每一片施法(根据可用资源,一次处理n片?) (4) 将它们打包回一个文件(例如,打包成JSON数组?)

在这个类比中,它指的是每块蛋糕最多是64GB,而不是整个蛋糕是64GB。这就是文档所指的意思。

来自70年代的操作模式加密没有问题。我不明白为什么他们会在GCM文档中说谎。这意味着AEAD模式是辅助性的,但不会取代或替换传统模式。

不考虑32位。假设是64位机器。

这会生成一个单一的随机数和一个单一的标签吗?

Go团队对此反驳了一段时间:

proposal: crypto/cipher: GCM for stream encryption

proposal: x/crypto: add streaming AEAD interface

我认为他们不会实现。

我的问题是关于使用非AEAD模式加密大文件的可能性,但在使用AEAD模式时会出现内存错误。

可以分块处理文件,但这并非正确的方式。我的意思是,为了实现与其他同类工具的兼容性,我需要使用一种标准的加密方案,而不包含将文件分割成多个部分的额外步骤。我认为,通过分割文件,在AEAD模式下每个部分都会有不同的标签。

可以在CTR或CBC模式下加密一个2GB的文件,但在AEAD模式下会出现错误。

2007年时64位机器还很罕见,然而它在64位机器上也无法运行。

"在某种程度上,你可以说这是一种临时解决方案。虽然可以将数据分割成更小的块并分别加密,但这并非理想或推荐的做法。

此外,将数据分解成更小的块可能会损害加密的安全性,因为较小块中包含的信息可以被独立分析,从而增加了攻击成功的可能性。

因此,遵循监管机构和加密货币制造商设定的建议和限制非常重要,以确保数据安全并避免性能或安全问题。"

受够了这种不一致性。算法文档将明文的最大大小限制为64GB。

根据此处提供的解决方案,这个数字可以被外推。也就是说,使用此处给出的解决方案,没有最大明文大小限制,因为它会被无限分割,由硬件来定义明文的最大限制。

这个解决方案是一种变通方法,并没有解释为什么算法文档中将明文大小限制为64GB。

无论如何,感谢您的尝试。

如果我们打算直接从锅里喝汤,那就不存在勺子大小的问题。我的问题与烹饪无关。

是的,我知道这个算法,比如GCM,它在2007年发布(NIST SP 800-38D 第5.2.1节),声称可以加密64GB的明文,尽管OCB算法可以加密4PT。在2023年,我正在消除关于这纯属无稽之谈或是Go自身GCM实现限制的主要原因的疑虑。

事实上,这个问题是因为我的一个应用程序用户遇到了这个困难而产生的。我个人通常不加密任何东西,我只是研究它。

谢谢!

无论如何,可以通过 buf.Grow(3*1024*1024*1024) 将缓冲区大小增加到 3GB,但这超过了第八个梅森素数,因此在 32 位机器上无法实现。我需要根据文件大小动态分配缓冲区大小:

	buf := bytes.NewBuffer(nil)
	var data io.Reader
	var size int64
	data = os.Stdin
	fi, err := os.Stdin.Stat()
	if err != nil {
		log.Fatal(err)
	}
	size = fi.Size()
	buf.Grow(int(size))
	io.Copy(buf, data)
	msg := buf.Bytes()

但这同样不起作用。

这一限制基于加密算法的数学特性,该算法使用一个128位的计数器来生成加密块。

64GB的最大限制是由最大计数器大小为2^128这一事实决定的,即可用的密文块总数为2^128个。由于每个密文块长度为128位,最大明文大小为2^128 x 128位,约合687亿GB。然而,NIST SP 800-38D文档设定了64GB的限制,以确保加密数据的完整性和真实性。这样做是为了避免攻击者可能迫使计数器溢出,从而导致数据认证错误。

因此,尽管GCM模式理论上(仅)支持最大64GB的明文大小,但在实践中,由于内存不足问题,这是不可能实现的。

这正是我所期望的解释。

谁能从技术角度解释一下,为什么无法使用AEAD模式加密大文件?

我的意思是,使用非AEAD操作模式可以加密高达1.5GB的文件(这是我测试过的),然而,使用AEAD模式时,明文的最大尺寸仅为250MB。也就是说,在32位机器上,我无法使用AEAD模式加密超过250MB的文件。更大的文件会导致内存错误。

NIST SP800-38D 第5.2.1.1节指出,明文的最大长度是 2^39-256 位,约等于 64 GB。

假设 buf.Grow(3 * 1024 * 1024 * 1024) 将缓冲区大小增加到3GB,那么 buf.Grow(64 * 1024 * 1024 * 1024) 同样会导致内存错误,无法加密64GB。

应该如何操作?

提前感谢。

pedroalbanese:

可以分块处理文件,但这并不是正确的方法。我的意思是,为了实现与其他同类工具的兼容性,我需要使用一种标准的加密方案,而不是通过将文件分割成多个部分来增加额外的步骤。

pedroalbanese:

假设 buf.Grow(3 * 1024 * 1024 * 1024) 将缓冲区大小增加到 3GB,buf.Grow(64 * 1024 * 1024 * 1024) 同样会导致内存错误,无法加密 64GB 的文件。

该如何进行?

那么你需要研究一下你希望兼容的解密器算法。我真诚地认为你的方法(一个庞大的文件)行不通。否则,AES-XTS 就无法用于加密整个磁盘了。

据我所知,机密性涵盖了加密器/解密器。否则,加密就毫无意义。

pedroalbanese:

在2023年,我正试图弄清楚这纯粹是无稽之谈,还是Go自身GCM实现的限制。

很好。你是否知道32位机器最多只支持4GB的内存?如果来了一个32GB的负载怎么办?

pedroalbanese:

事实上,这个问题是因为我的一个应用程序用户遇到了这个困难才产生的。我个人通常不加密任何东西,我只是研究它。

Go 100%肯定不是问题,AES密码(GCM、XTS等)也不是。否则,Go会被大量不留情面的CVE报告淹没,人们也会对它敬而远之。

问题在于应用程序算法在设计时没有充分考虑,直接采用了全文件负载的方式,而不是对大文件采用流式处理。

如果我们回到蛋糕的比喻:

  1. 这个人决定一口气吃掉整个蛋糕。
  2. 不要因为叉子或勺子(密码)的尺寸小而责怪它们的设计。

基本上,GCM模式使用Seal和Open,不能与StreamWriter或StreamReader一起使用,这迫使我必须将整个文件加载到内存中。显然,对于大文件来说,这并不理想。

我认为,实现StreamWriter和StreamReader是crypto/cipher库团队的责任,以便像CTR模式那样使用:

	stream := cipher.NewCTR(ciph, iv)
	buf := make([]byte, 128*1<<10)
	var n int
	for {
		n, err = os.Stdin.Read(buf)
		if err != nil && err != io.EOF {
			panic(err)
		}
		stream.XORKeyStream(buf[:n], buf[:n])
		if _, err := os.Stdout.Write(buf[:n]); err != nil {
			panic(err)
		}
		if err == io.EOF {
			break
		}
	}

但在我看来,他们似乎并没有太专注于这个目标,而我又不想重新实现我的工具。

你正在使用在内存中加密字节块的代码。

据我所知,这是“就地”发生的。

因此,即使在最佳情况下(你并不具备),你至少需要两倍于要加密数据大小的内存。

由于32位机器最多只能寻址4GiB,你的最大输入大小是 (4 GiB - 操作系统使用的内存 - 其他软件使用的内存 - 程序其他部分使用的内存) / 2。所以最多2 GiB,前提是没有其他东西使用内存(这不现实)。实际上,这个值很可能在1到1.5 GiB的范围内。

解决这个问题的常用方法称为“分块”或“流式”处理。

你始终只在内存中保留输入文件的一个小“窗口”,并对其进行加密,同时携带算法所需的任何“状态”,以便将下一个“窗口”视为原始输入数据的一部分。

然后逐窗口写回加密结果。第一个字节不会因为最后一个字节而改变……

不要将此与分块的独立加密混淆!

不过,我对AEAD或Go的实现了解不够,无法说明如何在Go中正确地进行流式加密/解密。

在32位机器上处理大文件时,需要采用流式处理方式,避免一次性加载整个文件到内存。以下是修改后的示例代码:

package main

import (
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"encoding/hex"
	"flag"
	"fmt"
	"io"
	"log"
	"os"
)

var (
	dec   = flag.Bool("d", false, "Decrypt instead Encrypt.")
	key   = flag.String("k", "", "32-byte key in hex")
	chunk = flag.Int("c", 64*1024, "Chunk size in bytes")
)

func main() {
	flag.Parse()

	// 解析密钥
	keyBytes, err := hex.DecodeString(*key)
	if err != nil || len(keyBytes) != 32 {
		log.Fatal("Invalid 256-bit key")
	}

	// 创建AEAD实例
	block, err := aes.NewCipher(keyBytes)
	if err != nil {
		log.Fatal(err)
	}

	aead, err := cipher.NewGCM(block)
	if err != nil {
		log.Fatal(err)
	}

	nonceSize := aead.NonceSize()
	overhead := aead.Overhead()

	if !*dec {
		// 加密模式
		nonce := make([]byte, nonceSize)
		if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
			log.Fatal(err)
		}

		// 写入nonce
		if _, err := os.Stdout.Write(nonce); err != nil {
			log.Fatal(err)
		}

		// 流式加密
		buf := make([]byte, *chunk)
		for {
			n, err := os.Stdin.Read(buf)
			if err != nil && err != io.EOF {
				log.Fatal(err)
			}
			if n == 0 {
				break
			}

			// 加密当前chunk
			ciphertext := aead.Seal(nil, nonce, buf[:n], nil)
			if _, err := os.Stdout.Write(ciphertext); err != nil {
				log.Fatal(err)
			}
		}
	} else {
		// 解密模式
		nonce := make([]byte, nonceSize)
		if _, err := io.ReadFull(os.Stdin, nonce); err != nil {
			log.Fatal(err)
		}

		// 流式解密
		buf := make([]byte, *chunk+overhead)
		for {
			n, err := os.Stdin.Read(buf)
			if err != nil && err != io.EOF {
				log.Fatal(err)
			}
			if n == 0 {
				break
			}

			// 解密当前chunk
			plaintext, err := aead.Open(nil, nonce, buf[:n], nil)
			if err != nil {
				log.Fatal(err)
			}
			if _, err := os.Stdout.Write(plaintext); err != nil {
				log.Fatal(err)
			}
		}
	}
}

使用示例:

# 生成密钥
key=$(openssl rand -hex 32)

# 加密2GB文件
./encrypt -k "$key" < input.bin > encrypted.bin

# 解密文件
./encrypt -d -k "$key" < encrypted.bin > decrypted.bin

关键改进点:

  1. 使用固定大小的缓冲区进行流式处理
  2. 分块加密/解密,避免一次性加载大文件
  3. 每个chunk独立加密,使用相同的nonce
  4. 默认chunk大小为64KB,可根据32位内存限制调整

注意:GCM模式要求每个nonce的唯一性,但可以重复使用相同的nonce加密多个chunk,只要保证整个会话中nonce不重复即可。

回到顶部