From 39089cb9afc5017edd18e2af0b5ca3b500f3fd84 Mon Sep 17 00:00:00 2001 From: George Goldberg Date: Tue, 13 Feb 2024 20:38:45 +0000 Subject: [PATCH] Add hasing with a secret salt of every file. Salt is part of LegalHold object, and each file is preceeded by the salt when doing the SHA512 hash. File name and hash is written to `hashes.csv` file. Fixes #11 --- server/legalhold/legal_hold.go | 90 ++++++++++++++++++++++++++++- server/legalhold/legal_hold_test.go | 55 ++++++++++++++++++ server/model/legal_hold.go | 2 + server/store/kvstore/legal_hold.go | 1 + 4 files changed, 147 insertions(+), 1 deletion(-) diff --git a/server/legalhold/legal_hold.go b/server/legalhold/legal_hold.go index f9177fa..1039a3c 100644 --- a/server/legalhold/legal_hold.go +++ b/server/legalhold/legal_hold.go @@ -2,8 +2,11 @@ package legalhold import ( "bytes" + "crypto/sha512" + "encoding/csv" "encoding/json" "fmt" + "io" "strings" "github.com/gocarina/gocsv" @@ -177,6 +180,18 @@ func (ex *Execution) WritePostsBatchToFile(channelID string, posts []model.Legal csvReader := strings.NewReader(csvContent) _, err = ex.fileBackend.WriteFile(csvReader, path) + if err != nil { + return err + } + + hashReader := strings.NewReader(csvContent) + + h, err := hash(ex.LegalHold.Secret, hashReader) + if err != nil { + return err + } + + err = ex.WriteFileHash(path, h) return err } @@ -206,9 +221,20 @@ func (ex *Execution) ExportFiles(channelID string, batchCreateAt int64, batchPos if err != nil { return err } + + hashReader, err := ex.fileBackend.Reader(fileInfo.Path) + if err != nil { + return err + } + + h, err := hash(ex.LegalHold.Secret, hashReader) + if err != nil { + return err + } + + err = ex.WriteFileHash(fileInfo.Path, h) } - // Write the return nil } @@ -291,9 +317,55 @@ func (ex *Execution) UpdateIndexes() error { reader := bytes.NewReader(data) _, err = ex.fileBackend.WriteFile(reader, filePath) + if err != nil { + return err + } + + hashReader := bytes.NewReader(data) + if err != nil { + return err + } + + h, err := hash(ex.LegalHold.Secret, hashReader) + if err != nil { + return err + } + + err = ex.WriteFileHash(filePath, h) + return err } +func (ex *Execution) WriteFileHash(path, hash string) error { + hashesFilePath := fmt.Sprintf("%s/hashes.csv", ex.basePath()) + + var buf bytes.Buffer + writer := csv.NewWriter(&buf) + err := writer.Write([]string{path, hash}) + if err != nil { + return err + } + writer.Flush() + + lineReader := bytes.NewReader(buf.Bytes()) + + if exists, err := ex.fileBackend.FileExists(hashesFilePath); err != nil { + return err + } else if !exists { + _, err = ex.fileBackend.WriteFile(lineReader, hashesFilePath) + if err != nil { + return err + } + } else { + _, err = ex.fileBackend.AppendFile(lineReader, hashesFilePath) + if err != nil { + return err + } + } + + return nil +} + // basePath returns the base file storage path for this Execution. func (ex *Execution) basePath() string { return ex.LegalHold.BasePath() @@ -333,3 +405,19 @@ func (ex *Execution) filePath(channelID string, batchCreateAt int64, batchPostID fileName, ) } + +func hash(secret string, reader io.Reader) (string, error) { + hasher := sha512.New() + + _, err := hasher.Write([]byte(secret)) + if err != nil { + return "", err + } + + _, err = io.Copy(hasher, reader) + if err != nil { + return "", err + } + + return fmt.Sprintf("%x", hasher.Sum(nil)), nil +} diff --git a/server/legalhold/legal_hold_test.go b/server/legalhold/legal_hold_test.go index 9439991..63ffc7e 100644 --- a/server/legalhold/legal_hold_test.go +++ b/server/legalhold/legal_hold_test.go @@ -1,6 +1,7 @@ package legalhold import ( + "bytes" "testing" mattermostModel "github.com/mattermost/mattermost-server/v6/model" @@ -49,3 +50,57 @@ func TestApp_LegalHoldExecution_Execute(t *testing.T) { // TODO: Do some proper assertions here to really test the functionality. } + +func TestLegalHold_Hash(t *testing.T) { + testCases := []struct { + name string + input string + secret string + expectedOutput string + expectedError error + }{ + { + name: "empty input", + input: "", + secret: "foo", + expectedOutput: "f7fbba6e0636f890e56fbbf3283e524c6fa3204ae298382d624741d0dc6638326e282c41be5e4254d8820772c5518a2c5a8c0c7f7eda19594a7eb539453e1ed7", + expectedError: nil, + }, + { + name: "valid input", + input: "Hello, World!", + secret: "foo", + expectedOutput: "1964d4b69d3631a6ff90143c75ae1fb5c5c6045600c0dc52f1db1e2155028b56159d5d281479221f4d38fee22239dab46528424c2b122b62c97e75f01f409f4d", + expectedError: nil, + }, + { + name: "valid input", + input: "Hello, World!", + secret: "", + expectedOutput: "374d794a95cdcfd8b35993185fef9ba368f160d8daf432d08ba9f1ed1e5abe6cc69291e0fa2fe0006a52570ef18c19def4e617c33ce52ef0a6e5fbe318cb0387", + expectedError: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + reader := bytes.NewReader([]byte(tc.input)) + result, err := hash(tc.secret, reader) + + if err != nil { + if tc.expectedError == nil { + t.Errorf("hash() with args %v : Unexpected error %v", tc.input, err) + } else if err.Error() != tc.expectedError.Error() { + t.Errorf("hash() with args %v : expected %v, got %v", + tc.input, tc.expectedError, err) + } + } else { + if tc.expectedOutput != result { + t.Errorf("hash() with args %v : expected %v, got %v", + tc.input, tc.expectedOutput, result) + } + } + + }) + } +} diff --git a/server/model/legal_hold.go b/server/model/legal_hold.go index e99a80a..833a203 100644 --- a/server/model/legal_hold.go +++ b/server/model/legal_hold.go @@ -21,6 +21,7 @@ type LegalHold struct { EndsAt int64 `json:"ends_at"` LastExecutionEndedAt int64 `json:"last_execution_ended_at"` ExecutionLength int64 `json:"execution_length"` + Secret string `json:"secret"` } // DeepCopy creates a deep copy of the LegalHold. @@ -39,6 +40,7 @@ func (lh *LegalHold) DeepCopy() LegalHold { EndsAt: lh.EndsAt, LastExecutionEndedAt: lh.LastExecutionEndedAt, ExecutionLength: lh.ExecutionLength, + Secret: lh.Secret, } if len(lh.UserIDs) > 0 { diff --git a/server/store/kvstore/legal_hold.go b/server/store/kvstore/legal_hold.go index 190f73a..a9f0470 100644 --- a/server/store/kvstore/legal_hold.go +++ b/server/store/kvstore/legal_hold.go @@ -31,6 +31,7 @@ func (kvs Impl) CreateLegalHold(lh model.LegalHold) (*model.LegalHold, error) { lh.CreateAt = mattermostModel.GetMillis() lh.UpdateAt = lh.CreateAt + lh.Secret = mattermostModel.NewId() key := fmt.Sprintf("%s%s", legalHoldPrefix, lh.ID)