diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index be92b64..13df020 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go-version: ["1.25.x"] + go-version: ["1.26.x"] steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 41b3e54..49ee8d7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go-version: ["1.25.x"] + go-version: ["1.26.x"] steps: - uses: actions/checkout@v5 diff --git a/aescbc/aescbc.go b/aescbc/aescbc.go index 89bc153..25c0f7d 100644 --- a/aescbc/aescbc.go +++ b/aescbc/aescbc.go @@ -28,6 +28,11 @@ func Decrypt(key []byte, ciphertext []byte) ([]byte, error) { iv := ciphertext[:aes.BlockSize] ciphertext = ciphertext[aes.BlockSize:] + + if len(ciphertext) == 0 || len(ciphertext)%aes.BlockSize != 0 { + return nil, fmt.Errorf("ciphertext after IV must be a non-zero multiple of %d bytes but was %d", aes.BlockSize, len(ciphertext)) + } + decrypter := cipher.NewCBCDecrypter(block, iv) plaintextData := make([]byte, len(ciphertext)) diff --git a/aescbc/aescbc_test.go b/aescbc/aescbc_test.go index 4598afe..aa6f068 100644 --- a/aescbc/aescbc_test.go +++ b/aescbc/aescbc_test.go @@ -30,6 +30,39 @@ func TestDecrypt(t *testing.T) { assert.Equal(t, []byte("Hello world"), dec) } +func TestDecryptBlockSizeValidation(t *testing.T) { + key := []byte("12345678901234567890123456789012") // 32 bytes + + t.Run("IV only, no data", func(t *testing.T) { + ciphertext := make([]byte, 16) // exactly aes.BlockSize + _, err := aescbc.Decrypt(key, ciphertext) + assert.ErrorContains(t, err, "ciphertext after IV must be a non-zero multiple of 16 bytes but was 0") + }) + + t.Run("non-block-aligned after IV", func(t *testing.T) { + ciphertext := make([]byte, 17) // aes.BlockSize + 1 + _, err := aescbc.Decrypt(key, ciphertext) + assert.ErrorContains(t, err, "ciphertext after IV must be a non-zero multiple of 16 bytes but was 1") + }) + + t.Run("block-aligned after IV does not panic", func(t *testing.T) { + ciphertext := make([]byte, 32) // aes.BlockSize + aes.BlockSize + assert.NotPanics(t, func() { + // Will fail on padding validation, but must not panic + _, _ = aescbc.Decrypt(key, ciphertext) + }) + }) + + t.Run("round-trip", func(t *testing.T) { + plaintext := []byte("security audit fix") + enc, err := aescbc.Encrypt(rand.Reader, key, plaintext) + require.NoError(t, err) + dec, err := aescbc.Decrypt(key, enc) + require.NoError(t, err) + assert.Equal(t, plaintext, dec) + }) +} + func FuzzEncryptAndDecrypt(f *testing.F) { f.Add(uint64(0), uint64(0), uint64(0), uint64(0), []byte("hello")) f.Fuzz(func(t *testing.T, u0, u1, u2, u3 uint64, plaintext []byte) {