From cdc698800656206f5c62b999134e2e5121f5b2d5 Mon Sep 17 00:00:00 2001 From: Keith Fong Date: Fri, 18 Sep 2026 16:32:26 +0800 Subject: [PATCH 1/3] Add explicit-IV DES CBC decryption --- README.md | 3 +- VERSION.txt | 2 +- des/des_cbc_cipher.go | 22 ++++++++++++ des/des_cbc_cipher_test.go | 70 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 des/des_cbc_cipher.go create mode 100644 des/des_cbc_cipher_test.go diff --git a/README.md b/README.md index c823a0e..6a8fb28 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,8 @@ AES-CBC does not authenticate ciphertext. Callers must authenticate the initiali ### DES - factory methods to construct an DES or 3DES cipher from the raw key bytes or hex text -- encrypt & decrypt methods +- ECB encrypt & decrypt methods +- CBC decryption with an explicit 8-byte IV, without padding removal or authentication - verify the constructed cipher against the check value ### KEK Bundle diff --git a/VERSION.txt b/VERSION.txt index cd99d38..141f2e8 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -1.14.0 \ No newline at end of file +1.15.0 diff --git a/des/des_cbc_cipher.go b/des/des_cbc_cipher.go new file mode 100644 index 0000000..e752c99 --- /dev/null +++ b/des/des_cbc_cipher.go @@ -0,0 +1,22 @@ +package des + +import ( + "crypto/cipher" + "fmt" +) + +// DecryptCBC decrypts non-empty, block-aligned ciphertext using the supplied IV. +// It does not remove padding or authenticate the ciphertext. +func (c *Cipher) DecryptCBC(cipherBytes, iv []byte) ([]byte, error) { + blockSize := c.KeyBlock.BlockSize() + if len(iv) != blockSize { + return nil, fmt.Errorf("IV must be %d bytes", blockSize) + } + if len(cipherBytes) == 0 || len(cipherBytes)%blockSize != 0 { + return nil, fmt.Errorf("ciphertext must be a non-empty multiple of %d bytes", blockSize) + } + + plainBytes := make([]byte, len(cipherBytes)) + cipher.NewCBCDecrypter(c.KeyBlock, iv).CryptBlocks(plainBytes, cipherBytes) + return plainBytes, nil +} diff --git a/des/des_cbc_cipher_test.go b/des/des_cbc_cipher_test.go new file mode 100644 index 0000000..ff190c0 --- /dev/null +++ b/des/des_cbc_cipher_test.go @@ -0,0 +1,70 @@ +package des + +import ( + "bytes" + "encoding/hex" + "testing" +) + +func TestTripleDESDecryptCBC(t *testing.T) { + // Independent Python cryptography/OpenSSL vectors: plaintext bytes 00..1f, + // no padding. The first two cases use double-length 3DES (K1, K2, K1). + tests := []struct { + name, key, iv, ciphertext string + }{ + {"double-length zero IV", "0123456789ABCDEFFEDCBA9876543210", "0000000000000000", "52c5c0705d9089e1fd2f0cf61dd3bc0ac0a3e54b8860076a1ec9453c5a31bf0f"}, + {"double-length nonzero IV", "0123456789ABCDEFFEDCBA9876543210", "1234567890abcdef", "188508514eeb60d24f8e125f9c2754e977bcc79e1c71911de0f7ed47b084f209"}, + {"triple-length zero IV", "0123456789ABCDEFFEDCBA987654321089ABCDEF01234567", "0000000000000000", "98f447d72bee8df72fc757753238f778ad817caff7455eadc9121c8a11c8ad16"}, + {"triple-length nonzero IV", "0123456789ABCDEFFEDCBA987654321089ABCDEF01234567", "1234567890abcdef", "dab10e76a31ef60dda6931da2244e63db6eec753a1774b4e7a6663c9805ff5f6"}, + } + expected := make([]byte, 32) + for i := range expected { + expected[i] = byte(i) + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c, err := CreateFromTripleDESKeyString(tt.key) + if err != nil { + t.Fatal(err) + } + iv, _ := hex.DecodeString(tt.iv) + ciphertext, _ := hex.DecodeString(tt.ciphertext) + for i := 0; i < 2; i++ { + plaintext, err := c.DecryptCBC(ciphertext, iv) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(plaintext, expected) { + t.Fatal("plaintext does not match the independent vector") + } + } + if hex.EncodeToString(ciphertext) != tt.ciphertext || hex.EncodeToString(iv) != tt.iv { + t.Fatal("decryption modified the ciphertext or IV") + } + }) + } +} + +func TestDecryptCBCRejectsInvalidInput(t *testing.T) { + c, err := CreateFromTripleDESKeyString("0123456789ABCDEFFEDCBA9876543210") + if err != nil { + t.Fatal(err) + } + tests := []struct { + name string + ciphertext, iv []byte + }{ + {"missing IV", make([]byte, 8), nil}, + {"short IV", make([]byte, 8), make([]byte, 7)}, + {"long IV", make([]byte, 8), make([]byte, 9)}, + {"empty ciphertext", nil, make([]byte, 8)}, + {"unaligned ciphertext", make([]byte, 9), make([]byte, 8)}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if plaintext, err := c.DecryptCBC(tt.ciphertext, tt.iv); err == nil || plaintext != nil { + t.Fatal("expected an error and no plaintext") + } + }) + } +} From 82ca634a39b052749b342f16d9c663e675a864d9 Mon Sep 17 00:00:00 2001 From: Keith Fong <86591198+keithfong94@users.noreply.github.com> Date: Fri, 18 Sep 2026 16:46:55 +0800 Subject: [PATCH 2/3] Add Apache License header to des_cbc_cipher.go Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- des/des_cbc_cipher.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/des/des_cbc_cipher.go b/des/des_cbc_cipher.go index e752c99..debe575 100644 --- a/des/des_cbc_cipher.go +++ b/des/des_cbc_cipher.go @@ -1,3 +1,14 @@ +/* + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ package des import ( From 16f484bd8a97565b0b4d69fa7e96ffdcbc6d63d5 Mon Sep 17 00:00:00 2001 From: Keith Fong <86591198+keithfong94@users.noreply.github.com> Date: Fri, 18 Sep 2026 16:47:09 +0800 Subject: [PATCH 3/3] Add Apache License header to des_cbc_cipher_test.go Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- des/des_cbc_cipher_test.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/des/des_cbc_cipher_test.go b/des/des_cbc_cipher_test.go index ff190c0..7512895 100644 --- a/des/des_cbc_cipher_test.go +++ b/des/des_cbc_cipher_test.go @@ -1,3 +1,14 @@ +/* + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ package des import (