From 9c7f6954e6d825170a90ae2ee7892776342081fa Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 26 Sep 2023 13:05:31 +0200 Subject: [PATCH] Add support for embedded text, json and yaml checksums --- cmd/cmd-checksum.go | 111 +++++++++++++++++++++ filesig/json.go | 197 ++++++++++++++++++++++++++++++++++++ filesig/json_test.go | 119 ++++++++++++++++++++++ filesig/text.go | 232 +++++++++++++++++++++++++++++++++++++++++++ filesig/text_test.go | 179 +++++++++++++++++++++++++++++++++ filesig/text_yaml.go | 11 ++ go.mod | 2 +- 7 files changed, 850 insertions(+), 1 deletion(-) create mode 100644 cmd/cmd-checksum.go create mode 100644 filesig/json.go create mode 100644 filesig/json_test.go create mode 100644 filesig/text.go create mode 100644 filesig/text_test.go create mode 100644 filesig/text_yaml.go diff --git a/cmd/cmd-checksum.go b/cmd/cmd-checksum.go new file mode 100644 index 0000000..0f8ccb2 --- /dev/null +++ b/cmd/cmd-checksum.go @@ -0,0 +1,111 @@ +package main + +import ( + "errors" + "fmt" + "os" + "path/filepath" + + "github.com/spf13/cobra" + + "github.com/safing/jess/filesig" +) + +func init() { + rootCmd.AddCommand(checksum) + checksum.AddCommand(checksumAdd) + checksum.AddCommand(checksumVerify) +} + +var ( + checksum = &cobra.Command{ + Use: "checksum", + Short: "add or verify embedded checksums", + } + + checksumAddUsage = "usage: checksum add " + checksumAdd = &cobra.Command{ + Use: "add ", + Short: "add an embedded checksum to a file", + Long: "add an embedded checksum to a file (support file types: txt, json, yaml)", + RunE: handleChecksumAdd, + } + + checksumVerifyUsage = "usage: checksum verify " + checksumVerify = &cobra.Command{ + Use: "verify ", + Short: "verify the embedded checksum of a file", + Long: "verify the embedded checksum of a file (support file types: txt, json, yaml)", + RunE: handleChecksumVerify, + } +) + +func handleChecksumAdd(cmd *cobra.Command, args []string) error { + // Check args. + if len(args) != 1 { + return errors.New(checksumAddUsage) + } + filename := args[0] + + data, err := os.ReadFile(filename) + if err != nil { + return fmt.Errorf("failed to read file: %w", err) + } + + switch filepath.Ext(filename) { + case ".json": + data, err = filesig.AddJSONChecksum(data) + case ".yml", ".yaml": + data, err = filesig.AddYAMLChecksum(data, filesig.TextPlacementAfterComment) + case ".txt": + data, err = filesig.AddTextFileChecksum(data, "#", filesig.TextPlacementAfterComment) + default: + return errors.New("unsupported file format") + } + if err != nil { + return err + } + + // Write back to disk. + fileInfo, err := os.Stat(filename) + if err != nil { + return fmt.Errorf("failed to stat file: %w", err) + } + err = os.WriteFile(filename, data, fileInfo.Mode().Perm()) + if err != nil { + return fmt.Errorf("failed to write back file with checksum: %w", err) + } + + fmt.Println("checksum added") + return nil +} + +func handleChecksumVerify(cmd *cobra.Command, args []string) error { + // Check args. + if len(args) != 1 { + return errors.New(checksumVerifyUsage) + } + filename := args[0] + + data, err := os.ReadFile(filename) + if err != nil { + return fmt.Errorf("failed to read file: %w", err) + } + + switch filepath.Ext(filename) { + case ".json": + err = filesig.VerifyJSONChecksum(data) + case ".yml", ".yaml": + err = filesig.VerifyYAMLChecksum(data) + case ".txt": + err = filesig.VerifyTextFileChecksum(data, "#") + default: + return errors.New("unsupported file format") + } + if err != nil { + return err + } + + fmt.Println("checksum verified") + return nil +} diff --git a/filesig/json.go b/filesig/json.go new file mode 100644 index 0000000..84906a0 --- /dev/null +++ b/filesig/json.go @@ -0,0 +1,197 @@ +package filesig + +import ( + "errors" + "fmt" + + "github.com/tidwall/gjson" + "github.com/tidwall/pretty" + "github.com/tidwall/sjson" + "golang.org/x/exp/slices" + + "github.com/safing/jess/lhash" +) + +// JSON file metadata keys. +const ( + JSONKeyPrefix = "_jess-" + JSONChecksumKey = JSONKeyPrefix + "checksum" + JSONSignatureKey = JSONKeyPrefix + "signature" +) + +// AddJSONChecksum adds a checksum to a text file. +func AddJSONChecksum(data []byte) ([]byte, error) { + // Extract content and metadata from json. + content, checksums, signatures, err := jsonSplit(data) + if err != nil { + return nil, err + } + + // Calculate checksum. + h := lhash.BLAKE2b_256.Digest(content) + checksums = append(checksums, h.Base58()) + + // Sort and deduplicate checksums and sigs. + slices.Sort[[]string, string](checksums) + checksums = slices.Compact[[]string, string](checksums) + slices.Sort[[]string, string](signatures) + signatures = slices.Compact[[]string, string](signatures) + + // Add metadata and return. + return jsonAddMeta(content, checksums, signatures) +} + +// VerifyJSONChecksum checks a checksum in a text file. +func VerifyJSONChecksum(data []byte) error { + // Extract content and metadata from json. + content, checksums, _, err := jsonSplit(data) + if err != nil { + return err + } + + // Verify all checksums. + var checksumsVerified int + for _, checksum := range checksums { + // Parse checksum. + h, err := lhash.FromBase58(checksum) + if err != nil { + return fmt.Errorf("%w: failed to parse labeled hash: %w", ErrChecksumFailed, err) + } + // Verify checksum. + if !h.Matches(content) { + return ErrChecksumFailed + } + checksumsVerified++ + } + + // Fail when no checksums were verified. + if checksumsVerified == 0 { + return ErrChecksumMissing + } + + return nil +} + +func jsonSplit(data []byte) ( + content []byte, + checksums []string, + signatures []string, + err error, +) { + // Check json. + if !gjson.ValidBytes(data) { + return nil, nil, nil, errors.New("invalid json") + } + content = data + + // Get checksums. + result := gjson.GetBytes(content, JSONChecksumKey) + if result.Exists() { + if result.IsArray() { + array := result.Array() + checksums = make([]string, 0, len(array)) + for _, result := range array { + if result.Type == gjson.String { + checksums = append(checksums, result.String()) + } + } + } else if result.Type == gjson.String { + checksums = []string{result.String()} + } + + // Delete key. + content, err = sjson.DeleteBytes(content, JSONChecksumKey) + if err != nil { + return nil, nil, nil, err + } + } + + // Get signatures. + result = gjson.GetBytes(content, JSONSignatureKey) + if result.Exists() { + if result.IsArray() { + array := result.Array() + signatures = make([]string, 0, len(array)) + for _, result := range array { + if result.Type == gjson.String { + signatures = append(signatures, result.String()) + } + } + } else if result.Type == gjson.String { + signatures = []string{result.String()} + } + + // Delete key. + content, err = sjson.DeleteBytes(content, JSONSignatureKey) + if err != nil { + return nil, nil, nil, err + } + } + + // Format for reproducible checksums and signatures. + content = pretty.PrettyOptions(content, &pretty.Options{ + Width: 200, // Must not change! + Prefix: "", // Must not change! + Indent: " ", // Must not change! + SortKeys: true, // Must not change! + }) + + return content, checksums, signatures, nil +} + +func jsonAddMeta(data []byte, checksums, signatures []string) ([]byte, error) { + var ( + err error + opts = &sjson.Options{ + ReplaceInPlace: true, + } + ) + + // Add checksums. + switch len(checksums) { + case 0: + // Skip + case 1: + // Add single checksum. + data, err = sjson.SetBytesOptions( + data, JSONChecksumKey, checksums[0], opts, + ) + default: + // Add multiple checksums. + data, err = sjson.SetBytesOptions( + data, JSONChecksumKey, checksums, opts, + ) + } + if err != nil { + return nil, err + } + + // Add signatures. + switch len(signatures) { + case 0: + // Skip + case 1: + // Add single signature. + data, err = sjson.SetBytesOptions( + data, JSONSignatureKey, signatures[0], opts, + ) + default: + // Add multiple signatures. + data, err = sjson.SetBytesOptions( + data, JSONSignatureKey, signatures, opts, + ) + } + if err != nil { + return nil, err + } + + // Final pretty print. + data = pretty.PrettyOptions(data, &pretty.Options{ + Width: 200, // Must not change! + Prefix: "", // Must not change! + Indent: " ", // Must not change! + SortKeys: true, // Must not change! + }) + + return data, nil +} diff --git a/filesig/json_test.go b/filesig/json_test.go new file mode 100644 index 0000000..a1505f7 --- /dev/null +++ b/filesig/json_test.go @@ -0,0 +1,119 @@ +package filesig + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestJSONChecksums(t *testing.T) { + t.Parallel() + + // Base test text file. + json := `{"a": "b", "c": 1}` + + // Test with checksum after comment. + + jsonWithChecksum := `{ + "_jess-checksum": "ZwtAd75qvioh6uf1NAq64KRgTbqeehFVYmhLmrwu1s7xJo", + "a": "b", + "c": 1 +} +` + + testJSONWithChecksum, err := AddJSONChecksum([]byte(json)) + assert.NoError(t, err, "should be able to add checksum") + assert.Equal(t, jsonWithChecksum, string(testJSONWithChecksum), "should match") + assert.NoError(t, + VerifyJSONChecksum(testJSONWithChecksum), + "checksum should be correct", + ) + + jsonWithChecksum = `{ + "c": 1, "a":"b", + "_jess-checksum": "ZwtAd75qvioh6uf1NAq64KRgTbqeehFVYmhLmrwu1s7xJo" + }` + assert.NoError(t, + VerifyJSONChecksum([]byte(jsonWithChecksum)), + "checksum should be correct", + ) + + jsonWithMultiChecksum := `{ + "_jess-checksum": [ + "PTV7S3Ca81aRk2kdNw7q2RfjLfEdPPT5Px5d211nhZedZC", + "PTV7S3Ca81aRk2kdNw7q2RfjLfEdPPT5Px5d211nhZedZC", + "CyDGH55DZUwa556DiYztMXaKZVBDjzWeFETiGmABMbvC3V" + ], + "a": "b", + "c": 1 + } + ` + assert.NoError(t, + VerifyJSONChecksum([]byte(jsonWithMultiChecksum)), + "checksum should be correct", + ) + + jsonWithMultiChecksumOutput := `{ + "_jess-checksum": ["CyDGH55DZUwa556DiYztMXaKZVBDjzWeFETiGmABMbvC3V", "PTV7S3Ca81aRk2kdNw7q2RfjLfEdPPT5Px5d211nhZedZC", "ZwtAd75qvioh6uf1NAq64KRgTbqeehFVYmhLmrwu1s7xJo"], + "a": "b", + "c": 1 +} +` + + testJSONWithMultiChecksum, err := AddJSONChecksum([]byte(jsonWithMultiChecksum)) + assert.NoError(t, err, "should be able to add checksum") + assert.Equal(t, jsonWithMultiChecksumOutput, string(testJSONWithMultiChecksum), "should match") + assert.NoError(t, + VerifyJSONChecksum(testJSONWithMultiChecksum), + "checksum should be correct", + ) + + // // Test with multiple checksums. + + // textWithMultiChecksum := `# jess-checksum: PTNktssvYCYjZXLFL2QoBk7DYoSz1qF7DJd5XNvtptd41B + // #!/bin/bash + // # Initial + // # Comment + // # Block + // # jess-checksum: Cy2TyVDjEStUqX3wCzCCKTfy228KaQK25ZDbHNmKiF8SPf + + // do_something() + + // # jess-checksum: YdgJFzuvFduk1MwRjZ2JkWQ6tCE1wkjn9xubSggKAdJSX5 + // ` + // assert.NoError(t, + // VerifyTextFileChecksum([]byte(textWithMultiChecksum), "#"), + // "checksum should be correct", + // ) + + // textWithMultiChecksumOutput := `#!/bin/bash + // # Initial + // # Comment + // # Block + // # jess-checksum: Cy2TyVDjEStUqX3wCzCCKTfy228KaQK25ZDbHNmKiF8SPf + // # jess-checksum: PTNktssvYCYjZXLFL2QoBk7DYoSz1qF7DJd5XNvtptd41B + // # jess-checksum: YdgJFzuvFduk1MwRjZ2JkWQ6tCE1wkjn9xubSggKAdJSX5 + // # jess-checksum: ZwngYUfUBeUn99HSdrNxkWSNjqrgZuSpVrexeEYttBso5o + + // do_something() + // ` + // testTextWithMultiChecksumOutput, err := AddTextFileChecksum([]byte(textWithMultiChecksum), "#", AfterComment) + // assert.NoError(t, err, "should be able to add checksum") + // assert.Equal(t, textWithMultiChecksumOutput, string(testTextWithMultiChecksumOutput), "should match") + + // // Test failing checksums. + + // textWithFailingChecksums := `#!/bin/bash + // # Initial + // # Comment + // # Block + // # jess-checksum: Cy2TyVDjEStUqX3wCzCCKTfy228KaQK25ZDbHNmKiF8SPf + // # jess-checksum: PTNktssvYCYjZXLFL2QoBk7DYoSz1qF7DJd5XNvtptd41B + // # jess-checksum: YdgJFzuvFduk1MwRjZ2JkWQ6tCE1wkjn9xubSggKAdJSX5 + // # jess-checksum: ZwngYUfUBeUn99HSdrNxkWSNjaaaaaaaaaaaaaaaaaaaaa + + // do_something() + // ` + // + // assert.Error(t, VerifyTextFileChecksum([]byte(textWithFailingChecksums), "#"), "should fail") +} diff --git a/filesig/text.go b/filesig/text.go new file mode 100644 index 0000000..a3c222a --- /dev/null +++ b/filesig/text.go @@ -0,0 +1,232 @@ +package filesig + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "io" + "strings" + + "golang.org/x/exp/slices" + + "github.com/safing/jess/lhash" +) + +// Text file metadata keys. +const ( + TextKeyPrefix = "jess-" + TextChecksumKey = TextKeyPrefix + "checksum" + TextSignatureKey = TextKeyPrefix + "signature" +) + +// Text Operation Errors. +var ( + ErrChecksumMissing = errors.New("no checksum found") + ErrChecksumFailed = errors.New("checksum does not match") + ErrSignatureMissing = errors.New("signature not found") + ErrSignatureFailed = errors.New("signature does not match") +) + +// TextPlacement signifies where jess metadata is put in text files. +type TextPlacement string + +const ( + // TextPlacementTop places the metadata at end of file. + TextPlacementTop TextPlacement = "top" + // TextPlacementBottom places the metadata at end of file. + TextPlacementBottom TextPlacement = "bottom" + // TextPlacementAfterComment places the metadata at end of the top comment + // block, or at the top, if the first line is not a comment. + TextPlacementAfterComment TextPlacement = "after-comment" + + defaultMetaPlacement = TextPlacementAfterComment +) + +// AddTextFileChecksum adds a checksum to a text file. +func AddTextFileChecksum(data []byte, commentSign string, placement TextPlacement) ([]byte, error) { + // Split text file into content and jess metadata lines. + content, metaLines, err := textSplit(data, commentSign) + if err != nil { + return nil, err + } + + // Calculate checksum. + h := lhash.BLAKE2b_256.Digest(content) + metaLines = append(metaLines, TextChecksumKey+": "+h.Base58()) + + // Sort and deduplicate meta lines. + slices.Sort[[]string, string](metaLines) + metaLines = slices.Compact[[]string, string](metaLines) + + // Add meta lines and return. + return textAddMeta(content, metaLines, commentSign, placement) +} + +// VerifyTextFileChecksum checks a checksum in a text file. +func VerifyTextFileChecksum(data []byte, commentSign string) error { + // Split text file into content and jess metadata lines. + content, metaLines, err := textSplit(data, commentSign) + if err != nil { + return err + } + + // Verify all checksums. + var checksumsVerified int + for _, line := range metaLines { + if strings.HasPrefix(line, TextChecksumKey) { + // Clean key, delimiters and space. + line = strings.TrimPrefix(line, TextChecksumKey) + line = strings.TrimSpace(line) // Spaces and newlines. + line = strings.Trim(line, ":= ") // Delimiters and spaces. + // Parse checksum. + h, err := lhash.FromBase58(line) + if err != nil { + return fmt.Errorf("%w: failed to parse labeled hash: %w", ErrChecksumFailed, err) + } + // Verify checksum. + if !h.Matches(content) { + return ErrChecksumFailed + } + checksumsVerified++ + } + } + + // Fail when no checksums were verified. + if checksumsVerified == 0 { + return ErrChecksumMissing + } + + return nil +} + +func textSplit(data []byte, commentSign string) (content []byte, metaLines []string, err error) { + metaLinePrefix := commentSign + " " + TextKeyPrefix + contentBuf := bytes.NewBuffer(make([]byte, 0, len(data))) + metaLines = make([]string, 0, 1) + + // Find jess metadata lines. + s := bufio.NewScanner(bytes.NewReader(data)) + s.Split(scanRawLines) + for s.Scan() { + if strings.HasPrefix(s.Text(), metaLinePrefix) { + metaLines = append(metaLines, strings.TrimSpace(strings.TrimPrefix(s.Text(), commentSign))) + } else { + _, _ = contentBuf.Write(s.Bytes()) + } + } + if s.Err() != nil { + return nil, nil, s.Err() + } + + return bytes.TrimSpace(contentBuf.Bytes()), metaLines, nil +} + +func detectLineEndFormat(data []byte) (lineEnd string) { + i := bytes.IndexByte(data, '\n') + switch i { + case -1: + // Default to just newline. + return "\n" + case 0: + // File start with a newline. + return "\n" + default: + // First newline is at second byte or later. + if bytes.Equal(data[i-1:i+1], []byte("\r\n")) { + return "\r\n" + } + return "\n" + } +} + +func textAddMeta(data []byte, metaLines []string, commentSign string, position TextPlacement) ([]byte, error) { + // Prepare new buffer. + requiredSize := len(data) + for _, line := range metaLines { + requiredSize += len(line) + len(commentSign) + 3 // space + CRLF + } + contentBuf := bytes.NewBuffer(make([]byte, 0, requiredSize)) + + // Find line ending. + lineEnd := detectLineEndFormat(data) + + // Find jess metadata lines. + if position == "" { + position = defaultMetaPlacement + } + + switch position { + case TextPlacementTop: + textWriteMetaLines(metaLines, commentSign, lineEnd, contentBuf) + contentBuf.Write(data) + // Add final newline. + contentBuf.WriteString(lineEnd) + + case TextPlacementBottom: + contentBuf.Write(data) + // Add to newlines when appending, as content is first whitespace-stripped. + contentBuf.WriteString(lineEnd) + contentBuf.WriteString(lineEnd) + textWriteMetaLines(metaLines, commentSign, lineEnd, contentBuf) + + case TextPlacementAfterComment: + metaWritten := false + s := bufio.NewScanner(bytes.NewReader(data)) + s.Split(scanRawLines) + for s.Scan() { + switch { + case metaWritten: + _, _ = contentBuf.Write(s.Bytes()) + case strings.HasPrefix(s.Text(), commentSign): + _, _ = contentBuf.Write(s.Bytes()) + default: + textWriteMetaLines(metaLines, commentSign, lineEnd, contentBuf) + metaWritten = true + _, _ = contentBuf.Write(s.Bytes()) + } + } + if s.Err() != nil { + return nil, s.Err() + } + // If we have scanned through the file, and meta was not written, write it now. + if !metaWritten { + textWriteMetaLines(metaLines, commentSign, lineEnd, contentBuf) + } + // Add final newline. + contentBuf.WriteString(lineEnd) + } + + return contentBuf.Bytes(), nil +} + +func textWriteMetaLines(metaLines []string, commentSign string, lineEnd string, writer io.StringWriter) { + for _, line := range metaLines { + _, _ = writer.WriteString(commentSign) + _, _ = writer.WriteString(" ") + _, _ = writer.WriteString(line) + _, _ = writer.WriteString(lineEnd) + } +} + +// scanRawLines is a split function for a Scanner that returns each line of +// text, including any trailing end-of-line marker. The returned line may +// be empty. The end-of-line marker is one optional carriage return followed +// by one mandatory newline. In regular expression notation, it is `\r?\n`. +// The last non-empty line of input will be returned even if it has no +// newline. +func scanRawLines(data []byte, atEOF bool) (advance int, token []byte, err error) { + if atEOF && len(data) == 0 { + return 0, nil, nil + } + if i := bytes.IndexByte(data, '\n'); i >= 0 { + // We have a full newline-terminated line. + return i + 1, data[0 : i+1], nil + } + // If we're at EOF, we have a final, non-terminated line. Return it. + if atEOF { + return len(data), data, nil + } + // Request more data. + return 0, nil, nil +} diff --git a/filesig/text_test.go b/filesig/text_test.go new file mode 100644 index 0000000..e896c75 --- /dev/null +++ b/filesig/text_test.go @@ -0,0 +1,179 @@ +package filesig + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestTextChecksums(t *testing.T) { + t.Parallel() + + // Base test text file. + text := `#!/bin/bash +# Initial +# Comment +# Block + +do_something()` + + // Test with checksum after comment. + + textWithChecksumAfterComment := `#!/bin/bash +# Initial +# Comment +# Block +# jess-checksum: ZwngYUfUBeUn99HSdrNxkWSNjqrgZuSpVrexeEYttBso5o + +do_something() +` + + testTextWithChecksumAfterComment, err := AddTextFileChecksum([]byte(text), "#", TextPlacementAfterComment) + assert.NoError(t, err, "should be able to add checksum") + assert.Equal(t, textWithChecksumAfterComment, string(testTextWithChecksumAfterComment), "should match") + assert.NoError(t, + VerifyTextFileChecksum(testTextWithChecksumAfterComment, "#"), + "checksum should be correct", + ) + assert.NoError(t, + VerifyTextFileChecksum(append( + []byte("\n\n \r\n"), + testTextWithChecksumAfterComment..., + ), "#"), + "checksum should be correct", + ) + assert.NoError(t, + VerifyTextFileChecksum(append( + testTextWithChecksumAfterComment, + []byte("\r\n \n \n")..., + ), "#"), + "checksum should be correct", + ) + + // Test with checksum at top. + + textWithChecksumAtTop := `# jess-checksum: ZwngYUfUBeUn99HSdrNxkWSNjqrgZuSpVrexeEYttBso5o +#!/bin/bash +# Initial +# Comment +# Block + +do_something() +` + + testTextWithChecksumAtTop, err := AddTextFileChecksum([]byte(text), "#", TextPlacementTop) + assert.NoError(t, err, "should be able to add checksum") + assert.Equal(t, textWithChecksumAtTop, string(testTextWithChecksumAtTop), "should match") + assert.NoError(t, + VerifyTextFileChecksum(testTextWithChecksumAtTop, "#"), + "checksum should be correct", + ) + + // Test with checksum at bottom. + + textWithChecksumAtBottom := `#!/bin/bash +# Initial +# Comment +# Block + +do_something() + +# jess-checksum: ZwngYUfUBeUn99HSdrNxkWSNjqrgZuSpVrexeEYttBso5o +` + + testTextWithChecksumAtBottom, err := AddTextFileChecksum([]byte(text), "#", TextPlacementBottom) + assert.NoError(t, err, "should be able to add checksum") + assert.Equal(t, textWithChecksumAtBottom, string(testTextWithChecksumAtBottom), "should match") + assert.NoError(t, + VerifyTextFileChecksum(testTextWithChecksumAtBottom, "#"), + "checksum should be correct", + ) + + // Test with multiple checksums. + + textWithMultiChecksum := `# jess-checksum: PTNktssvYCYjZXLFL2QoBk7DYoSz1qF7DJd5XNvtptd41B +#!/bin/bash +# Initial +# Comment +# Block +# jess-checksum: Cy2TyVDjEStUqX3wCzCCKTfy228KaQK25ZDbHNmKiF8SPf + +do_something() + +# jess-checksum: YdgJFzuvFduk1MwRjZ2JkWQ6tCE1wkjn9xubSggKAdJSX5 +` + assert.NoError(t, + VerifyTextFileChecksum([]byte(textWithMultiChecksum), "#"), + "checksum should be correct", + ) + + textWithMultiChecksumOutput := `#!/bin/bash +# Initial +# Comment +# Block +# jess-checksum: Cy2TyVDjEStUqX3wCzCCKTfy228KaQK25ZDbHNmKiF8SPf +# jess-checksum: PTNktssvYCYjZXLFL2QoBk7DYoSz1qF7DJd5XNvtptd41B +# jess-checksum: YdgJFzuvFduk1MwRjZ2JkWQ6tCE1wkjn9xubSggKAdJSX5 +# jess-checksum: ZwngYUfUBeUn99HSdrNxkWSNjqrgZuSpVrexeEYttBso5o + +do_something() +` + testTextWithMultiChecksumOutput, err := AddTextFileChecksum([]byte(textWithMultiChecksum), "#", TextPlacementAfterComment) + assert.NoError(t, err, "should be able to add checksum") + assert.Equal(t, textWithMultiChecksumOutput, string(testTextWithMultiChecksumOutput), "should match") + + // Test failing checksums. + + textWithFailingChecksums := `#!/bin/bash +# Initial +# Comment +# Block +# jess-checksum: Cy2TyVDjEStUqX3wCzCCKTfy228KaQK25ZDbHNmKiF8SPf +# jess-checksum: PTNktssvYCYjZXLFL2QoBk7DYoSz1qF7DJd5XNvtptd41B +# jess-checksum: YdgJFzuvFduk1MwRjZ2JkWQ6tCE1wkjn9xubSggKAdJSX5 +# jess-checksum: ZwngYUfUBeUn99HSdrNxkWSNjaaaaaaaaaaaaaaaaaaaaa + +do_something() +` + assert.Error(t, VerifyTextFileChecksum([]byte(textWithFailingChecksums), "#"), "should fail") +} + +func TestLineEndDetection(t *testing.T) { + t.Parallel() + + assert.Equal(t, + "\n", + detectLineEndFormat(nil), + "empty data should default to simple lf ending", + ) + assert.Equal(t, + "\n", + detectLineEndFormat([]byte("\n")), + "shoud detect lf ending with empty first line", + ) + assert.Equal(t, + "\r\n", + detectLineEndFormat([]byte("\r\n")), + "shoud detect crlf ending with empty first line", + ) + assert.Equal(t, + "\n", + detectLineEndFormat([]byte("abc\n")), + "shoud detect lf ending with data on single line", + ) + assert.Equal(t, + "\r\n", + detectLineEndFormat([]byte("abc\r\n")), + "shoud detect crlf ending with data on single line", + ) + assert.Equal(t, + "\n", + detectLineEndFormat([]byte("abc\nabc\r\n")), + "shoud detect lf ending with data on first line", + ) + assert.Equal(t, + "\r\n", + detectLineEndFormat([]byte("abc\r\nabc\n")), + "shoud detect crlf ending with data on first line", + ) +} diff --git a/filesig/text_yaml.go b/filesig/text_yaml.go new file mode 100644 index 0000000..dd8a1e2 --- /dev/null +++ b/filesig/text_yaml.go @@ -0,0 +1,11 @@ +package filesig + +// AddYAMLChecksum adds a checksum to a yaml file. +func AddYAMLChecksum(data []byte, placement TextPlacement) ([]byte, error) { + return AddTextFileChecksum(data, "#", placement) +} + +// VerifyYAMLChecksum checks a checksum in a yaml file. +func VerifyYAMLChecksum(data []byte) error { + return VerifyTextFileChecksum(data, "#") +} diff --git a/go.mod b/go.mod index 0ef93fe..3c55613 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/safing/jess -go 1.15 +go 1.20 require ( github.com/AlecAivazis/survey/v2 v2.3.6