init: AIGC-Hub/AVCC 方案文档 + TCS-IPTV 内容可信锁定系统 MVP
- 方案文档: AVCC 体系建设、IPTV TCS 需求(0-req)/PRD(1-prd)/任务(2-task)/二三四期任务 - tcs-iptv: Go 后端(哈希SDK/MA码生成/可信数据空间mock/业务编排/HTTP API+HMAC鉴权) - web-console: React+AntD 监管大屏(角色工作台/全流程演示/监管片库) - 一剧一码+集级哈希, 集级下架/恢复, 全栈测试通过
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
package hash
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func writeTempFile(t *testing.T, data []byte) string {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
p := filepath.Join(dir, "master.bin")
|
||||
require.NoError(t, os.WriteFile(p, data, 0o644))
|
||||
return p
|
||||
}
|
||||
|
||||
func TestSHA256Hex_Deterministic(t *testing.T) {
|
||||
a := SHA256Hex([]byte("hello"))
|
||||
b := SHA256Hex([]byte("hello"))
|
||||
assert.Equal(t, a, b)
|
||||
// 已知向量
|
||||
assert.Equal(t, "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824", a)
|
||||
}
|
||||
|
||||
func TestFileSHA256_MatchesBytes(t *testing.T) {
|
||||
data := []byte("the quick brown fox")
|
||||
p := writeTempFile(t, data)
|
||||
got, err := FileSHA256(p)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, SHA256Hex(data), got)
|
||||
}
|
||||
|
||||
func TestSegmentHashes_SmallSegments(t *testing.T) {
|
||||
// 25 字节,分段 10 → 3 段(10/10/5)
|
||||
data := []byte("0123456789ABCDEFGHIJ12345")
|
||||
p := writeTempFile(t, data)
|
||||
segs, err := SegmentHashes(p, 10)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, segs, 3)
|
||||
assert.Equal(t, SHA256Hex(data[0:10]), segs[0])
|
||||
assert.Equal(t, SHA256Hex(data[10:20]), segs[1])
|
||||
assert.Equal(t, SHA256Hex(data[20:25]), segs[2])
|
||||
}
|
||||
|
||||
func TestMerkleTree_RootStableAndChangesOnEdit(t *testing.T) {
|
||||
leaves := []string{
|
||||
SHA256Hex([]byte("ep1")),
|
||||
SHA256Hex([]byte("ep2")),
|
||||
SHA256Hex([]byte("ep3")),
|
||||
SHA256Hex([]byte("ep4")),
|
||||
}
|
||||
root1 := BuildMerkleTree(leaves).Root()
|
||||
root2 := BuildMerkleTree(leaves).Root()
|
||||
assert.Equal(t, root1, root2, "同样叶子根应一致")
|
||||
assert.NotEmpty(t, root1)
|
||||
|
||||
// 改第3集 → 根变化
|
||||
edited := append([]string(nil), leaves...)
|
||||
edited[2] = SHA256Hex([]byte("ep3-tampered"))
|
||||
root3 := BuildMerkleTree(edited).Root()
|
||||
assert.NotEqual(t, root1, root3, "篡改任一集,根必变")
|
||||
}
|
||||
|
||||
func TestMerkleTree_OddLeaves(t *testing.T) {
|
||||
leaves := []string{
|
||||
SHA256Hex([]byte("a")),
|
||||
SHA256Hex([]byte("b")),
|
||||
SHA256Hex([]byte("c")),
|
||||
}
|
||||
mt := BuildMerkleTree(leaves)
|
||||
assert.NotEmpty(t, mt.Root())
|
||||
}
|
||||
|
||||
func TestLocateChangedLeaves(t *testing.T) {
|
||||
old := []string{"h1", "h2", "h3", "h4"}
|
||||
neu := []string{"h1", "x2", "h3", "x4"}
|
||||
changed := LocateChangedLeaves(old, neu)
|
||||
assert.Equal(t, []int{1, 3}, changed, "应定位到第2集和第4集被改")
|
||||
}
|
||||
|
||||
func TestComputeFile_FullPackage(t *testing.T) {
|
||||
data := make([]byte, 25*1024) // 25KB
|
||||
for i := range data {
|
||||
data[i] = byte(i % 251)
|
||||
}
|
||||
p := writeTempFile(t, data)
|
||||
|
||||
pkg, err := ComputeFile(p, Options{SegmentSize: 10 * 1024})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, pkg.Validate())
|
||||
assert.Equal(t, int64(25*1024), pkg.FileSize)
|
||||
assert.Len(t, pkg.SegmentHashes, 3)
|
||||
assert.NotEmpty(t, pkg.MerkleRoot)
|
||||
assert.Equal(t, SHA256Hex(data), pkg.FileSHA256)
|
||||
}
|
||||
|
||||
func TestComputeFile_EmptyFileRejected(t *testing.T) {
|
||||
p := writeTempFile(t, []byte{})
|
||||
_, err := ComputeFile(p, Options{})
|
||||
assert.ErrorIs(t, err, ErrEmptyInput)
|
||||
}
|
||||
|
||||
func TestComputeFile_MissingFile(t *testing.T) {
|
||||
_, err := ComputeFile("/no/such/file.bin", Options{})
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestHashPackage_ValidateMissingFields(t *testing.T) {
|
||||
assert.Error(t, (&HashPackage{MerkleRoot: "x"}).Validate()) // 缺 file_sha256
|
||||
assert.Error(t, (&HashPackage{FileSHA256: "x"}).Validate()) // 缺 merkle_root
|
||||
assert.NoError(t, (&HashPackage{FileSHA256: "a", MerkleRoot: "b"}).Validate())
|
||||
}
|
||||
|
||||
func TestPerceptualHash_IdenticalAndDifferent(t *testing.T) {
|
||||
// 全黑与全白图,aHash/dHash 应可区分
|
||||
black := make([][]uint8, 16)
|
||||
white := make([][]uint8, 16)
|
||||
grad := make([][]uint8, 16)
|
||||
for y := 0; y < 16; y++ {
|
||||
black[y] = make([]uint8, 16)
|
||||
white[y] = make([]uint8, 16)
|
||||
grad[y] = make([]uint8, 16)
|
||||
for x := 0; x < 16; x++ {
|
||||
white[y][x] = 255
|
||||
grad[y][x] = uint8(x * 16) // 水平渐变
|
||||
}
|
||||
}
|
||||
imgBlack := newGrayTestImage(black)
|
||||
imgWhite := newGrayTestImage(white)
|
||||
imgGrad := newGrayTestImage(grad)
|
||||
|
||||
// 同一图的哈希稳定
|
||||
assert.Equal(t, AHash(imgGrad), AHash(imgGrad))
|
||||
assert.Equal(t, DHash(imgGrad), DHash(imgGrad))
|
||||
|
||||
// 渐变图的 dHash 应与纯色不同
|
||||
assert.NotEqual(t, DHash(imgGrad), DHash(imgBlack))
|
||||
|
||||
// 汉明距离:渐变 vs 纯白 应 > 0
|
||||
d, err := HammingDistance(DHash(imgGrad), DHash(imgWhite))
|
||||
require.NoError(t, err)
|
||||
assert.Greater(t, d, 0)
|
||||
}
|
||||
|
||||
func TestHammingDistance_LengthMismatch(t *testing.T) {
|
||||
_, err := HammingDistance("ffff", "ffffffff")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package hash
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
)
|
||||
|
||||
// MerkleTree 表示一棵基于 SHA-256 的 Merkle 树。
|
||||
// 叶子为各分段(或各集)的哈希,根用于整体内容的聚合锚定。
|
||||
// 对应需求:需求1-AC2、需求16-AC4(按集定位篡改)。
|
||||
type MerkleTree struct {
|
||||
Leaves []string // 叶子哈希(十六进制)
|
||||
Levels [][]string // 自底向上的各层,Levels[0] 为叶子层
|
||||
}
|
||||
|
||||
// BuildMerkleTree 由叶子哈希构建 Merkle 树。
|
||||
// 当某层节点数为奇数时,复制最后一个节点与自身配对(标准做法)。
|
||||
func BuildMerkleTree(leaves []string) *MerkleTree {
|
||||
mt := &MerkleTree{Leaves: append([]string(nil), leaves...)}
|
||||
if len(leaves) == 0 {
|
||||
mt.Levels = [][]string{{}}
|
||||
return mt
|
||||
}
|
||||
|
||||
level := append([]string(nil), leaves...)
|
||||
mt.Levels = [][]string{level}
|
||||
|
||||
for len(level) > 1 {
|
||||
next := make([]string, 0, (len(level)+1)/2)
|
||||
for i := 0; i < len(level); i += 2 {
|
||||
left := level[i]
|
||||
right := left // 奇数个时与自身配对
|
||||
if i+1 < len(level) {
|
||||
right = level[i+1]
|
||||
}
|
||||
next = append(next, hashPair(left, right))
|
||||
}
|
||||
mt.Levels = append(mt.Levels, next)
|
||||
level = next
|
||||
}
|
||||
return mt
|
||||
}
|
||||
|
||||
// Root 返回 Merkle 根哈希;空树返回空字符串。
|
||||
func (mt *MerkleTree) Root() string {
|
||||
if len(mt.Levels) == 0 {
|
||||
return ""
|
||||
}
|
||||
top := mt.Levels[len(mt.Levels)-1]
|
||||
if len(top) == 0 {
|
||||
return ""
|
||||
}
|
||||
return top[0]
|
||||
}
|
||||
|
||||
// hashPair 将两个十六进制哈希拼接后再次 SHA-256。
|
||||
func hashPair(left, right string) string {
|
||||
lb, _ := hex.DecodeString(left)
|
||||
rb, _ := hex.DecodeString(right)
|
||||
h := sha256.New()
|
||||
h.Write(lb)
|
||||
h.Write(rb)
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
// LocateChangedLeaves 比较两组叶子哈希,返回发生变化的叶子索引。
|
||||
// 用于"定位被篡改的具体集"(需求12-AC3)。
|
||||
func LocateChangedLeaves(oldLeaves, newLeaves []string) []int {
|
||||
var changed []int
|
||||
max := len(oldLeaves)
|
||||
if len(newLeaves) > max {
|
||||
max = len(newLeaves)
|
||||
}
|
||||
for i := 0; i < max; i++ {
|
||||
var o, n string
|
||||
if i < len(oldLeaves) {
|
||||
o = oldLeaves[i]
|
||||
}
|
||||
if i < len(newLeaves) {
|
||||
n = newLeaves[i]
|
||||
}
|
||||
if o != n {
|
||||
changed = append(changed, i)
|
||||
}
|
||||
}
|
||||
return changed
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package hash
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"math/bits"
|
||||
)
|
||||
|
||||
// 感知哈希用于跨格式/转码识别同一内容(需求1-AC3)。
|
||||
// MVP 实现 aHash(均值哈希)与 dHash(差值哈希),输入为已解码的视频代表帧图像。
|
||||
// 真实视频抽帧由上层(ffmpeg)完成,本包专注哈希算法以便独立测试。
|
||||
|
||||
const phashDim = 8 // 8x8 → 64-bit 哈希
|
||||
|
||||
// grayResize 将图像缩放为 w×h 的灰度矩阵(最近邻,零依赖)。
|
||||
func grayResize(img image.Image, w, h int) [][]float64 {
|
||||
b := img.Bounds()
|
||||
srcW, srcH := b.Dx(), b.Dy()
|
||||
out := make([][]float64, h)
|
||||
for y := 0; y < h; y++ {
|
||||
out[y] = make([]float64, w)
|
||||
for x := 0; x < w; x++ {
|
||||
sx := b.Min.X + x*srcW/w
|
||||
sy := b.Min.Y + y*srcH/h
|
||||
r, g, bb, _ := img.At(sx, sy).RGBA()
|
||||
// 转 8 位灰度(ITU-R 601 亮度)
|
||||
gray := 0.299*float64(r>>8) + 0.587*float64(g>>8) + 0.114*float64(bb>>8)
|
||||
out[y][x] = gray
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// AHash 计算均值哈希(64-bit,十六进制 16 字符)。
|
||||
func AHash(img image.Image) string {
|
||||
m := grayResize(img, phashDim, phashDim)
|
||||
var sum float64
|
||||
for y := 0; y < phashDim; y++ {
|
||||
for x := 0; x < phashDim; x++ {
|
||||
sum += m[y][x]
|
||||
}
|
||||
}
|
||||
avg := sum / float64(phashDim*phashDim)
|
||||
|
||||
var hash uint64
|
||||
var bit uint
|
||||
for y := 0; y < phashDim; y++ {
|
||||
for x := 0; x < phashDim; x++ {
|
||||
if m[y][x] >= avg {
|
||||
hash |= 1 << bit
|
||||
}
|
||||
bit++
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("%016x", hash)
|
||||
}
|
||||
|
||||
// DHash 计算差值哈希(64-bit)。对水平相邻像素比较亮度。
|
||||
func DHash(img image.Image) string {
|
||||
// 需要 (phashDim+1) 列以产生 phashDim 个差值
|
||||
m := grayResize(img, phashDim+1, phashDim)
|
||||
var hash uint64
|
||||
var bit uint
|
||||
for y := 0; y < phashDim; y++ {
|
||||
for x := 0; x < phashDim; x++ {
|
||||
if m[y][x] < m[y][x+1] {
|
||||
hash |= 1 << bit
|
||||
}
|
||||
bit++
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("%016x", hash)
|
||||
}
|
||||
|
||||
// HammingDistance 计算两个等长十六进制哈希的汉明距离。
|
||||
// 距离越小越相似;用于版权比对与跨版本识别。
|
||||
func HammingDistance(a, b string) (int, error) {
|
||||
if len(a) != len(b) {
|
||||
return 0, fmt.Errorf("hash: length mismatch %d vs %d", len(a), len(b))
|
||||
}
|
||||
var va, vb uint64
|
||||
if _, err := fmt.Sscanf(a, "%x", &va); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if _, err := fmt.Sscanf(b, "%x", &vb); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return bits.OnesCount64(va ^ vb), nil
|
||||
}
|
||||
|
||||
// newGrayTestImage 是测试辅助:由灰度矩阵生成图像。
|
||||
func newGrayTestImage(gray [][]uint8) image.Image {
|
||||
h := len(gray)
|
||||
w := 0
|
||||
if h > 0 {
|
||||
w = len(gray[0])
|
||||
}
|
||||
img := image.NewGray(image.Rect(0, 0, w, h))
|
||||
for y := 0; y < h; y++ {
|
||||
for x := 0; x < w; x++ {
|
||||
img.SetGray(x, y, color.Gray{Y: gray[y][x]})
|
||||
}
|
||||
}
|
||||
return img
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package hash
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// HashPackage 是哈希值包,对应需求1-AC5。
|
||||
// 仅包含哈希与元数据,绝不包含原始内容(需求20-AC2)。
|
||||
type HashPackage struct {
|
||||
FileSHA256 string `json:"file_sha256"`
|
||||
MerkleRoot string `json:"merkle_root"`
|
||||
SegmentHashes []string `json:"segment_hashes"`
|
||||
PerceptualHash string `json:"perceptual_hash,omitempty"`
|
||||
FileSize int64 `json:"file_size"`
|
||||
SegmentSize int `json:"segment_size"`
|
||||
}
|
||||
|
||||
// Options 控制哈希计算行为。
|
||||
type Options struct {
|
||||
SegmentSize int // 分段大小;<=0 用默认
|
||||
PerceptualHash string // 上层已抽帧并算好的感知哈希(可选)
|
||||
}
|
||||
|
||||
// ComputeFile 对母版文件计算完整哈希值包(文件哈希 + 分段 Merkle)。
|
||||
// 感知哈希需上层先抽帧,再通过 opts.PerceptualHash 传入或单独调用 AHash/DHash。
|
||||
func ComputeFile(path string, opts Options) (*HashPackage, error) {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("hash: stat file: %w", err)
|
||||
}
|
||||
if info.IsDir() {
|
||||
return nil, fmt.Errorf("hash: path is a directory: %s", path)
|
||||
}
|
||||
if info.Size() == 0 {
|
||||
return nil, ErrEmptyInput
|
||||
}
|
||||
|
||||
segSize := opts.SegmentSize
|
||||
if segSize <= 0 {
|
||||
segSize = DefaultSegmentSize
|
||||
}
|
||||
|
||||
fileHash, err := FileSHA256(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("hash: file sha256: %w", err)
|
||||
}
|
||||
|
||||
segments, err := SegmentHashes(path, segSize)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("hash: segment hashes: %w", err)
|
||||
}
|
||||
|
||||
tree := BuildMerkleTree(segments)
|
||||
|
||||
return &HashPackage{
|
||||
FileSHA256: fileHash,
|
||||
MerkleRoot: tree.Root(),
|
||||
SegmentHashes: segments,
|
||||
PerceptualHash: opts.PerceptualHash,
|
||||
FileSize: info.Size(),
|
||||
SegmentSize: segSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Validate 校验哈希值包的完整性(需求2-AC5:缺文件哈希/Merkle根则非法)。
|
||||
func (p *HashPackage) Validate() error {
|
||||
if p == nil {
|
||||
return fmt.Errorf("hash: nil package")
|
||||
}
|
||||
if p.FileSHA256 == "" {
|
||||
return fmt.Errorf("hash: missing file_sha256")
|
||||
}
|
||||
if p.MerkleRoot == "" {
|
||||
return fmt.Errorf("hash: missing merkle_root")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// Package hash 实现 TCS-IPTV 的内容哈希核心:
|
||||
// 文件 SHA-256、分段 Merkle Tree、感知哈希。
|
||||
// 对应需求:需求1(母版哈希生成)。
|
||||
package hash
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
// DefaultSegmentSize 是分段哈希的默认分段大小(10 MiB)。
|
||||
const DefaultSegmentSize = 10 * 1024 * 1024
|
||||
|
||||
// ErrEmptyInput 表示输入为空。
|
||||
var ErrEmptyInput = errors.New("hash: empty input")
|
||||
|
||||
// SHA256Hex 计算字节切片的 SHA-256,返回十六进制字符串。
|
||||
func SHA256Hex(data []byte) string {
|
||||
sum := sha256.Sum256(data)
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
// FileSHA256 流式计算文件的整体 SHA-256,避免一次性载入大文件。
|
||||
func FileSHA256(path string) (string, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
h := sha256.New()
|
||||
if _, err := io.Copy(h, f); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
// SegmentHashes 按 segmentSize 分段计算文件各段的 SHA-256。
|
||||
// 用于构建 Merkle Tree 的叶子节点。
|
||||
func SegmentHashes(path string, segmentSize int) ([]string, error) {
|
||||
if segmentSize <= 0 {
|
||||
segmentSize = DefaultSegmentSize
|
||||
}
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var segments []string
|
||||
buf := make([]byte, segmentSize)
|
||||
for {
|
||||
n, err := io.ReadFull(f, buf)
|
||||
if n > 0 {
|
||||
segments = append(segments, SHA256Hex(buf[:n]))
|
||||
}
|
||||
if err == io.EOF || err == io.ErrUnexpectedEOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return segments, nil
|
||||
}
|
||||
Reference in New Issue
Block a user