diff --git a/keys/ecc.go b/keys/ecc.go new file mode 100644 index 000000000..ff16345d3 --- /dev/null +++ b/keys/ecc.go @@ -0,0 +1,56 @@ +package keys + +import ( + "encoding/binary" + "errors" + "hash/crc32" +) + +// ECC is used for anything that calculates an error-correcting code +type ECC interface { + // AddECC calculates an error-correcting code for the input + // returns an output with the code appended + AddECC([]byte) []byte + + // CheckECC verifies if the ECC is proper on the input and returns + // the data with the code removed, or an error + CheckECC([]byte) ([]byte, error) +} + +// NoECC is a no-op placeholder, kind of useless... except for tests +type NoECC struct{} + +var _ ECC = NoECC{} + +func (_ NoECC) AddECC(input []byte) []byte { return input } +func (_ NoECC) CheckECC(input []byte) ([]byte, error) { return input, nil } + +// CRC32 does the ieee crc32 polynomial check +type CRC32 struct{} + +var _ ECC = CRC32{} + +func (_ CRC32) AddECC(input []byte) []byte { + // get crc and convert to some bytes... + crc := crc32.ChecksumIEEE(input) + check := make([]byte, 4) + binary.BigEndian.PutUint32(check, crc) + + // append it to the input + output := append(input, check...) + return output +} + +func (_ CRC32) CheckECC(input []byte) ([]byte, error) { + if len(input) <= 4 { + return nil, errors.New("input too short, no checksum present") + } + cut := len(input) - 4 + data, check := input[:cut], input[cut:] + crc := binary.BigEndian.Uint32(check) + calc := crc32.ChecksumIEEE(data) + if crc != calc { + return nil, errors.New("Checksum does not match") + } + return data, nil +} diff --git a/keys/ecc_test.go b/keys/ecc_test.go new file mode 100644 index 000000000..f58bc7a9c --- /dev/null +++ b/keys/ecc_test.go @@ -0,0 +1,52 @@ +package keys + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + cmn "github.com/tendermint/tmlibs/common" +) + +// TestECCPasses makes sure that the AddECC/CheckECC methods are symetric +func TestECCPasses(t *testing.T) { + assert := assert.New(t) + + checks := []ECC{NoECC{}, CRC32{}} + + for _, check := range checks { + for i := 0; i < 2000; i++ { + numBytes := cmn.RandInt()%60 + 1 + data := cmn.RandBytes(numBytes) + + checked := check.AddECC(data) + res, err := check.CheckECC(checked) + if assert.Nil(err, "%v: %+v", check, err) { + assert.Equal(data, res, "%v", check) + } + } + } +} + +// TestECCFails makes sure random data will (usually) fail the checksum +func TestECCFails(t *testing.T) { + assert := assert.New(t) + + checks := []ECC{CRC32{}} + + attempts := 2000 + + for _, check := range checks { + failed := 0 + for i := 0; i < attempts; i++ { + numBytes := cmn.RandInt()%60 + 1 + data := cmn.RandBytes(numBytes) + _, err := check.CheckECC(data) + if err != nil { + failed += 1 + } + } + // we allow up to 1 falsely accepted checksums, as there are random matches + assert.InDelta(attempts, failed, 1, "%v", check) + } +}