feat: 新增XTS/CCM流式与KDF能力,补充安全测试并更新README/CHANGELOG

This commit is contained in:
2026-03-18 13:43:18 +08:00
parent e89350b56a
commit 4fa79744e8
44 changed files with 4636 additions and 77 deletions
+59 -7
View File
@@ -2,10 +2,11 @@ package filex
import (
"bufio"
crand "crypto/rand"
"errors"
"fmt"
"io"
"math/rand"
mrand "math/rand"
"os"
"path/filepath"
"regexp"
@@ -15,6 +16,8 @@ import (
"time"
)
var ErrInvalidSplitPattern = errors.New("split dst pattern must contain exactly one '*'")
func Attach(src, dst, output string) error {
fpsrc, err := os.Open(src)
if err != nil {
@@ -81,6 +84,9 @@ func SplitFile(src, dst string, num int, bynum bool, progress func(float64)) err
if num <= 0 {
return errors.New("num must be greater than zero")
}
if strings.Count(dst, "*") != 1 {
return ErrInvalidSplitPattern
}
fpsrc, err := os.Open(src)
if err != nil {
@@ -244,6 +250,8 @@ func MergeFile(src, dst string, progress func(float64)) error {
return nil
}
// FillWithRandom fills file with pseudo-random bytes generated by math/rand.
// It is fast but not cryptographically secure.
func FillWithRandom(path string, filesize, bufcap, bufnum int, progress func(float64)) error {
if filesize < 0 {
return errors.New("filesize must be non-negative")
@@ -258,7 +266,7 @@ func FillWithRandom(path string, filesize, bufcap, bufnum int, progress func(flo
bufcap = filesize
}
rand.Seed(time.Now().UnixNano())
r := mrand.New(mrand.NewSource(time.Now().UnixNano()))
fp, err := os.Create(path)
if err != nil {
@@ -267,18 +275,17 @@ func FillWithRandom(path string, filesize, bufcap, bufnum int, progress func(flo
defer fp.Close()
writer := bufio.NewWriter(fp)
defer writer.Flush()
if filesize == 0 {
reportProgress(progress, 0, 0)
return nil
return writer.Flush()
}
pool := make([][]byte, 0, bufnum)
for i := 0; i < bufnum; i++ {
b := make([]byte, bufcap)
for j := 0; j < bufcap; j++ {
b[j] = byte(rand.Intn(256))
b[j] = byte(r.Intn(256))
}
pool = append(pool, b)
}
@@ -289,14 +296,59 @@ func FillWithRandom(path string, filesize, bufcap, bufnum int, progress func(flo
if filesize-written < chunk {
chunk = filesize - written
}
buf := pool[rand.Intn(len(pool))][:chunk]
buf := pool[r.Intn(len(pool))][:chunk]
if _, err := writer.Write(buf); err != nil {
return err
}
written += chunk
reportProgress(progress, int64(written), int64(filesize))
}
return nil
return writer.Flush()
}
// FillWithCryptoRandom fills file with cryptographically secure random bytes from crypto/rand.
// Security is stronger than FillWithRandom, but throughput may be lower.
func FillWithCryptoRandom(path string, filesize, bufcap int, progress func(float64)) error {
if filesize < 0 {
return errors.New("filesize must be non-negative")
}
if bufcap <= 0 {
bufcap = 1
}
if bufcap > filesize && filesize > 0 {
bufcap = filesize
}
fp, err := os.Create(path)
if err != nil {
return err
}
defer fp.Close()
writer := bufio.NewWriter(fp)
if filesize == 0 {
reportProgress(progress, 0, 0)
return writer.Flush()
}
buf := make([]byte, bufcap)
written := 0
for written < filesize {
chunk := bufcap
if filesize-written < chunk {
chunk = filesize - written
}
if _, err := io.ReadFull(crand.Reader, buf[:chunk]); err != nil {
return err
}
if _, err := writer.Write(buf[:chunk]); err != nil {
return err
}
written += chunk
reportProgress(progress, int64(written), int64(filesize))
}
return writer.Flush()
}
func reportProgress(progress func(float64), current, total int64) {
+65
View File
@@ -0,0 +1,65 @@
package filex
import (
"bytes"
"os"
"path/filepath"
"testing"
)
func TestFillWithRandomAndCryptoRandom(t *testing.T) {
dir := t.TempDir()
pseudoPath := filepath.Join(dir, "pseudo.bin")
securePath := filepath.Join(dir, "secure.bin")
if err := FillWithRandom(pseudoPath, 2048, 128, 4, nil); err != nil {
t.Fatalf("FillWithRandom failed: %v", err)
}
if err := FillWithCryptoRandom(securePath, 2048, 128, nil); err != nil {
t.Fatalf("FillWithCryptoRandom failed: %v", err)
}
pseudoInfo, err := os.Stat(pseudoPath)
if err != nil {
t.Fatalf("stat pseudo file failed: %v", err)
}
if pseudoInfo.Size() != 2048 {
t.Fatalf("unexpected pseudo size: %d", pseudoInfo.Size())
}
secureInfo, err := os.Stat(securePath)
if err != nil {
t.Fatalf("stat secure file failed: %v", err)
}
if secureInfo.Size() != 2048 {
t.Fatalf("unexpected secure size: %d", secureInfo.Size())
}
pseudo, err := os.ReadFile(pseudoPath)
if err != nil {
t.Fatalf("read pseudo file failed: %v", err)
}
secure, err := os.ReadFile(securePath)
if err != nil {
t.Fatalf("read secure file failed: %v", err)
}
if bytes.Equal(secure, make([]byte, len(secure))) {
t.Fatalf("secure random output should not be all zero")
}
if bytes.Equal(pseudo, secure) {
t.Fatalf("pseudo and secure random outputs unexpectedly identical")
}
}
func TestFillWithRandomInvalidArgs(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "bad.bin")
if err := FillWithRandom(path, -1, 16, 1, nil); err == nil {
t.Fatalf("expected FillWithRandom negative filesize error")
}
if err := FillWithCryptoRandom(path, -1, 16, nil); err == nil {
t.Fatalf("expected FillWithCryptoRandom negative filesize error")
}
}
+36
View File
@@ -0,0 +1,36 @@
package filex
import (
"bytes"
"errors"
"os"
"path/filepath"
"testing"
)
func TestSplitFilePatternValidation(t *testing.T) {
dir := t.TempDir()
src := filepath.Join(dir, "src.bin")
if err := os.WriteFile(src, bytes.Repeat([]byte{0x7f}, 64), 0o600); err != nil {
t.Fatalf("WriteFile failed: %v", err)
}
if err := SplitFile(src, filepath.Join(dir, "part.bin"), 2, true, nil); !errors.Is(err, ErrInvalidSplitPattern) {
t.Fatalf("expected ErrInvalidSplitPattern for missing '*', got: %v", err)
}
if err := SplitFile(src, filepath.Join(dir, "part_*_*.bin"), 2, true, nil); !errors.Is(err, ErrInvalidSplitPattern) {
t.Fatalf("expected ErrInvalidSplitPattern for multiple '*', got: %v", err)
}
pattern := filepath.Join(dir, "part_*.bin")
if err := SplitFile(src, pattern, 2, true, nil); err != nil {
t.Fatalf("SplitFile valid pattern failed: %v", err)
}
if _, err := os.Stat(filepath.Join(dir, "part_0.bin")); err != nil {
t.Fatalf("part_0.bin not found: %v", err)
}
if _, err := os.Stat(filepath.Join(dir, "part_1.bin")); err != nil {
t.Fatalf("part_1.bin not found: %v", err)
}
}