Skip to content

Commit eff4ff5

Browse files
besendorfJanik Besendorf
andauthored
Add hashes manifest for streaming archives (#84)
Co-authored-by: Janik Besendorf <janik.besendorf@reporter-ohne-grenzen.de>
1 parent b234322 commit eff4ff5

3 files changed

Lines changed: 176 additions & 2 deletions

File tree

acquisition/acquisition.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,11 @@ func (a *Acquisition) Complete() {
148148
}
149149
}
150150

151+
err = a.EncryptedWriter.CreateHashList()
152+
if err != nil {
153+
log.ErrorExc("Failed to add hashes.csv to encrypted archive", err)
154+
}
155+
151156
// Close the encrypted writer
152157
err = a.EncryptedWriter.Close()
153158
if err != nil {

acquisition/encrypted_stream.go

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ package acquisition
88
import (
99
"archive/zip"
1010
"bytes"
11+
"crypto/sha256"
12+
"encoding/csv"
13+
"encoding/hex"
1114
"fmt"
15+
"hash"
1216
"io"
1317
"os"
1418
"path"
@@ -28,6 +32,25 @@ type EncryptedZipWriter struct {
2832
zipWriter *zip.Writer
2933
outputPath string
3034
closed bool
35+
hashes []*zipHash
36+
}
37+
38+
type zipHash struct {
39+
name string
40+
hasher hash.Hash
41+
}
42+
43+
type hashingWriter struct {
44+
writer io.Writer
45+
hasher hash.Hash
46+
}
47+
48+
func (hw *hashingWriter) Write(p []byte) (int, error) {
49+
n, err := hw.writer.Write(p)
50+
if n > 0 {
51+
_, _ = hw.hasher.Write(p[:n])
52+
}
53+
return n, err
3154
}
3255

3356
// NewEncryptedZipWriter creates a new encrypted zip writer if key.txt exists
@@ -87,6 +110,10 @@ func NewEncryptedZipWriter(uuid string) (*EncryptedZipWriter, error) {
87110

88111
// CreateFile creates a new file in the encrypted zip and returns a writer
89112
func (ezw *EncryptedZipWriter) CreateFile(name string) (io.Writer, error) {
113+
return ezw.createFile(name, true)
114+
}
115+
116+
func (ezw *EncryptedZipWriter) createFile(name string, trackHash bool) (io.Writer, error) {
90117
if err := ezw.checkClosed(); err != nil {
91118
return nil, err
92119
}
@@ -101,7 +128,25 @@ func (ezw *EncryptedZipWriter) CreateFile(name string) (io.Writer, error) {
101128
Modified: time.Now(),
102129
}
103130

104-
return ezw.zipWriter.CreateHeader(header)
131+
writer, err := ezw.zipWriter.CreateHeader(header)
132+
if err != nil {
133+
return nil, err
134+
}
135+
136+
if !trackHash {
137+
return writer, nil
138+
}
139+
140+
zipHash := &zipHash{
141+
name: name,
142+
hasher: sha256.New(),
143+
}
144+
ezw.hashes = append(ezw.hashes, zipHash)
145+
146+
return &hashingWriter{
147+
writer: writer,
148+
hasher: zipHash.hasher,
149+
}, nil
105150
}
106151

107152
func validateZipEntryName(name string) error {
@@ -151,6 +196,39 @@ func (ezw *EncryptedZipWriter) CreateFileFromReader(name string, src io.Reader)
151196
return nil
152197
}
153198

199+
// CreateHashList adds hashes.csv to the encrypted zip with SHA-256 hashes of
200+
// the plaintext zip entries written so far.
201+
func (ezw *EncryptedZipWriter) CreateHashList() error {
202+
if err := ezw.checkClosed(); err != nil {
203+
return err
204+
}
205+
206+
var buffer bytes.Buffer
207+
csvWriter := csv.NewWriter(&buffer)
208+
for _, zipHash := range ezw.hashes {
209+
if err := csvWriter.Write([]string{
210+
zipHash.name,
211+
hex.EncodeToString(zipHash.hasher.Sum(nil)),
212+
}); err != nil {
213+
return fmt.Errorf("failed to write hash entry for %q: %v", zipHash.name, err)
214+
}
215+
}
216+
csvWriter.Flush()
217+
if err := csvWriter.Error(); err != nil {
218+
return fmt.Errorf("failed to create hash list: %v", err)
219+
}
220+
221+
writer, err := ezw.createFile("hashes.csv", false)
222+
if err != nil {
223+
return fmt.Errorf("failed to create hashes.csv in zip: %v", err)
224+
}
225+
if _, err := writer.Write(buffer.Bytes()); err != nil {
226+
return fmt.Errorf("failed to write hashes.csv to zip: %v", err)
227+
}
228+
229+
return nil
230+
}
231+
154232
// CreateFileFromString creates a file with string content in the encrypted zip
155233
func (ezw *EncryptedZipWriter) CreateFileFromString(name, content string) error {
156234
return ezw.CreateFileFromReader(name, strings.NewReader(content))

acquisition/encrypted_stream_test.go

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,97 @@
11
package acquisition
22

3-
import "testing"
3+
import (
4+
"archive/zip"
5+
"bytes"
6+
"crypto/sha256"
7+
"encoding/csv"
8+
"encoding/hex"
9+
"io"
10+
"strings"
11+
"testing"
12+
)
13+
14+
func TestCreateHashListTracksPlaintextZipEntries(t *testing.T) {
15+
var archive bytes.Buffer
16+
ezw := &EncryptedZipWriter{
17+
zipWriter: zip.NewWriter(&archive),
18+
}
19+
20+
if err := ezw.CreateFileFromString("first.txt", "first content"); err != nil {
21+
t.Fatalf("CreateFileFromString() error = %v", err)
22+
}
23+
24+
writer, err := ezw.CreateFile("stream.bin")
25+
if err != nil {
26+
t.Fatalf("CreateFile() error = %v", err)
27+
}
28+
if _, err := writer.Write([]byte("streamed ")); err != nil {
29+
t.Fatalf("Write() error = %v", err)
30+
}
31+
if _, err := writer.Write([]byte("content")); err != nil {
32+
t.Fatalf("Write() error = %v", err)
33+
}
34+
35+
if err := ezw.CreateHashList(); err != nil {
36+
t.Fatalf("CreateHashList() error = %v", err)
37+
}
38+
if err := ezw.zipWriter.Close(); err != nil {
39+
t.Fatalf("zip Close() error = %v", err)
40+
}
41+
42+
reader, err := zip.NewReader(bytes.NewReader(archive.Bytes()), int64(archive.Len()))
43+
if err != nil {
44+
t.Fatalf("zip.NewReader() error = %v", err)
45+
}
46+
47+
files := make(map[string]string)
48+
for _, file := range reader.File {
49+
readCloser, err := file.Open()
50+
if err != nil {
51+
t.Fatalf("Open(%q) error = %v", file.Name, err)
52+
}
53+
content, err := io.ReadAll(readCloser)
54+
readCloser.Close()
55+
if err != nil {
56+
t.Fatalf("ReadAll(%q) error = %v", file.Name, err)
57+
}
58+
files[file.Name] = string(content)
59+
}
60+
61+
records, err := csv.NewReader(strings.NewReader(files["hashes.csv"])).ReadAll()
62+
if err != nil {
63+
t.Fatalf("ReadAll(hashes.csv) error = %v", err)
64+
}
65+
66+
gotHashes := make(map[string]string)
67+
for _, record := range records {
68+
if len(record) != 2 {
69+
t.Fatalf("hash record has %d fields, want 2: %#v", len(record), record)
70+
}
71+
gotHashes[record[0]] = record[1]
72+
}
73+
74+
wantHashes := map[string]string{
75+
"first.txt": sha256Hex("first content"),
76+
"stream.bin": sha256Hex("streamed content"),
77+
}
78+
if len(gotHashes) != len(wantHashes) {
79+
t.Fatalf("got %d hash records, want %d: %#v", len(gotHashes), len(wantHashes), gotHashes)
80+
}
81+
for name, wantHash := range wantHashes {
82+
if gotHashes[name] != wantHash {
83+
t.Fatalf("hash for %q = %q, want %q", name, gotHashes[name], wantHash)
84+
}
85+
}
86+
if _, ok := gotHashes["hashes.csv"]; ok {
87+
t.Fatal("hashes.csv should not include a hash record for itself")
88+
}
89+
}
90+
91+
func sha256Hex(content string) string {
92+
sum := sha256.Sum256([]byte(content))
93+
return hex.EncodeToString(sum[:])
94+
}
495

596
func TestValidateZipEntryName(t *testing.T) {
697
tests := []struct {

0 commit comments

Comments
 (0)