diff --git a/cipher/gcm_sm4_test.go b/cipher/gcm_sm4_test.go index 7bfca9c..be09fa2 100644 --- a/cipher/gcm_sm4_test.go +++ b/cipher/gcm_sm4_test.go @@ -397,7 +397,7 @@ func TestGCMCounterWrap(t *testing.T) { func TestSM4GCMRandom(t *testing.T) { key := []byte("0123456789ABCDEF") nonce := []byte("0123456789AB") - plaintext := make([]byte, 464) + plaintext := make([]byte, 0x198) io.ReadFull(rand.Reader, plaintext) c, err := sm4.NewCipher(key) @@ -410,11 +410,11 @@ func TestSM4GCMRandom(t *testing.T) { } got := aead.Seal(nil, nonce, plaintext, nil) - result, err := aead.Open(nil, nonce, got, nil) + result, err := aead.Open(got[:0], nonce, got, nil) if err != nil { t.Fatal(err) } if !bytes.Equal(result, plaintext) { - t.Error("gcm seal/open 464 bytes fail") + t.Error("gcm seal/open 408 bytes fail") } } diff --git a/cipher/hctr.go b/cipher/hctr.go new file mode 100644 index 0000000..f8ac95d --- /dev/null +++ b/cipher/hctr.go @@ -0,0 +1,212 @@ +package cipher + +import ( + _cipher "crypto/cipher" + "encoding/binary" + "errors" + + "github.com/emmansun/gmsm/internal/alias" + "github.com/emmansun/gmsm/internal/subtle" +) + +// A LengthPreservingMode represents a block cipher running in a length preserving mode (HCTR, +// HCTR2 etc). +type LengthPreservingMode interface { + // Encrypt encrypts a number of plaintext bytes. The length of + // src must be NOT smaller than block size. Dst and src must overlap + // entirely or not at all. + // + // If len(dst) < len(src), Encrypt should panic. It is acceptable + // to pass a dst bigger than src, and in that case, Encrypt will + // only update dst[:len(src)] and will not touch the rest of dst. + // + // Multiple calls to Encrypt behave NOT same as if the concatenation of + // the src buffers was passed in a single run. + Encrypt(dst, src []byte) + + // Decrypt decrypts a number of ciphertext bytes. The length of + // src must be NOT smaller than block size. Dst and src must overlap + // entirely or not at all. + // + // If len(dst) < len(src), Decrypt should panic. It is acceptable + // to pass a dst bigger than src, and in that case, Decrypt will + // only update dst[:len(src)] and will not touch the rest of dst. + // + // Multiple calls to Decrypt behave NOT same as if the concatenation of + // the src buffers was passed in a single run. + Decrypt(dst, src []byte) +} + +// hctr represents a Varaible-Input-Length enciphering mode with a specific block cipher, +// and specific tweak and a hash key. See +// https://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.470.5288 +// GB/T 17964-2021 第11章 带泛杂凑函数的计数器工作模式 +type hctr struct { + cipher _cipher.Block + tweak [blockSize]byte + hkey [blockSize]byte +} + +// NewHCTR returns a [LengthPreservingMode] which encrypts/decrypts useing the given [Block] +// in HCTR mode. The lenght of tweak and hash key must be the same as the [Block]'s block size. +func NewHCTR(cipher _cipher.Block, tweak, hkey []byte) (LengthPreservingMode, error) { + if len(tweak) != blockSize || len(hkey) != blockSize { + return nil, errors.New("hctr: invalid tweak and/or hash key length") + } + c := &hctr{} + c.cipher = cipher + copy(c.hkey[:], hkey) + copy(c.tweak[:], tweak) + return c, nil +} + +func _mul2(v *[blockSize]byte) { + var carryIn byte + for j := range v { + carryOut := (v[j] << 7) & 0x80 + v[j] = (v[j] >> 1) + carryIn + carryIn = carryOut + } + if carryIn != 0 { + v[0] ^= 0xE1 // 1<<7 | 1<<6 | 1<<5 | 1 + } +} + +// mul sets y to y*hkey. +func (h *hctr) mul(y *[blockSize]byte) { + var z [blockSize]byte + for _, i := range h.hkey { + for k := 0; k < 8; k++ { + if (i>>(7-k))&1 == 1 { + subtle.XORBytes(z[:], z[:], y[:]) + } + _mul2(y) + } + } + copy(y[:], z[:]) +} + +// Universal Hash Function. +// Chapter 3.3 in https://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.470.5288. +func (h *hctr) uhash(m []byte, dst *[blockSize]byte) { + for k := 0; k < blockSize; k++ { + dst[k] = 0 + } + msg := m + for len(msg) >= blockSize { + subtle.XORBytes(dst[:], dst[:], msg[:blockSize]) + h.mul(dst) + msg = msg[blockSize:] + } + var v [blockSize]byte + if len(msg) > 0 { + copy(v[:], msg) + copy(v[len(msg):], h.tweak[:]) + subtle.XORBytes(dst[:], dst[:], v[:]) + h.mul(dst) + copy(v[:], h.tweak[len(msg):]) + for i := len(msg); i < blockSize; i++ { + v[i] = 0 + } + subtle.XORBytes(dst[:], dst[:], v[:]) + h.mul(dst) + for i := 0; i < len(msg); i++ { + v[i] = 0 + } + } else { + subtle.XORBytes(dst[:], dst[:], h.tweak[:]) + h.mul(dst) + } + // (|M|)₂ + binary.BigEndian.PutUint64(v[8:], uint64(len(m)+blockSize)<<3) + subtle.XORBytes(dst[:], dst[:], v[:]) + h.mul(dst) +} + +func (h *hctr) Encrypt(ciphertext, plaintext []byte) { + if len(ciphertext) < len(plaintext) { + panic("hctr: ciphertext is smaller than plaintext") + } + if len(plaintext) < blockSize { + panic("hctr: plaintext length is smaller than the block size") + } + if alias.InexactOverlap(ciphertext[:len(plaintext)], plaintext) { + panic("hctr: invalid buffer overlap") + } + + var z1, z2 [blockSize]byte + + // a) z1 generation + h.uhash(plaintext[blockSize:], &z1) + subtle.XORBytes(z1[:], z1[:], plaintext[:blockSize]) + // b) z2 generation + h.cipher.Encrypt(z2[:], z1[:]) + // c) CTR + subtle.XORBytes(z1[:], z1[:], z2[:]) + h.ctr(ciphertext[blockSize:], plaintext[blockSize:], &z1) + // d) first ciphertext block generation + h.uhash(ciphertext[blockSize:], &z1) + subtle.XORBytes(ciphertext, z2[:], z1[:]) +} + +func (h *hctr) Decrypt(plaintext, ciphertext []byte) { + if len(plaintext) < len(ciphertext) { + panic("hctr: plaintext is smaller than cihpertext") + } + if len(ciphertext) < blockSize { + panic("hctr: ciphertext length is smaller than the block size") + } + if alias.InexactOverlap(plaintext[:len(ciphertext)], ciphertext) { + panic("hctr: invalid buffer overlap") + } + + var z1, z2 [blockSize]byte + + // a) z2 generation + h.uhash(ciphertext[blockSize:], &z2) + subtle.XORBytes(z2[:], z2[:], ciphertext[:blockSize]) + // b) z1 generation + h.cipher.Decrypt(z1[:], z2[:]) + // c) CTR + subtle.XORBytes(z2[:], z2[:], z1[:]) + h.ctr(plaintext[blockSize:], ciphertext[blockSize:], &z2) + // d) first plaintext block generation + h.uhash(plaintext[blockSize:], &z2) + subtle.XORBytes(plaintext, z2[:], z1[:]) +} + +func (h *hctr) ctr(dst, src []byte, baseCtr *[blockSize]byte) { + ctr := make([]byte, blockSize) + num := make([]byte, blockSize) + i := uint64(1) + + if concCipher, ok := h.cipher.(concurrentBlocks); ok { + batchSize := concCipher.Concurrency() * blockSize + if len(src) >= batchSize { + var ctrs []byte = make([]byte, batchSize) + for len(src) >= batchSize { + for j := 0; j < concCipher.Concurrency(); j++ { + // (i)₂ + binary.BigEndian.PutUint64(num[blockSize-8:], i) + subtle.XORBytes(ctrs[j*blockSize:], baseCtr[:], num) + i++ + } + concCipher.EncryptBlocks(ctrs, ctrs) + subtle.XORBytes(dst, src, ctrs) + src = src[batchSize:] + dst = dst[batchSize:] + } + } + } + + for len(src) > 0 { + // (i)₂ + binary.BigEndian.PutUint64(num[blockSize-8:], i) + subtle.XORBytes(ctr, baseCtr[:], num) + h.cipher.Encrypt(ctr, ctr) + n := subtle.XORBytes(dst, src, ctr) + src = src[n:] + dst = dst[n:] + i++ + } +} diff --git a/cipher/hctr_test.go b/cipher/hctr_test.go new file mode 100644 index 0000000..61d9ca5 --- /dev/null +++ b/cipher/hctr_test.go @@ -0,0 +1,75 @@ +package cipher_test + +import ( + "bytes" + "encoding/hex" + "testing" + + "github.com/emmansun/gmsm/cipher" + "github.com/emmansun/gmsm/sm4" +) + +var hctrSM4TestVectors = []struct { + key string + hashKey string + tweak string + plaintext string + ciphertext string +}{ + { + "2B7E151628AED2A6ABF7158809CF4F3C", + "000102030405060708090A0B0C0D0E0F", + "F0F1F2F3F4F5F6F7F8F9FAFBFCFDFEFF", + "6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c37106bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c37106bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c3710", + "8858dda3034233e377936b76ce7edeb6a245075a37800b0b996e8e974c9032ac8de40d90ee4ee5fb58bc10cbc95779485ab38ffb0b4f961d85f086db705ff723edbeaec649b3b406b11b96a418a9c2c51ef41cdd24e472c18336e9efcd07b7e264a1e2d46615198eb74938d72104fa89294a6360cdb6b032a704cf07a087bb2283598552701b2f710d6528d9c3f4dab529afef4413f25169b6cbf8168ccbfa02a2f507513d0cb3802da34dbd928b67e6afc30ca91011070cfd40c2ef3d4ac041", + }, + { + "2B7E151628AED2A6ABF7158809CF4F3C", + "000102030405060708090A0B0C0D0E0F", + "F0F1F2F3F4F5F6F7F8F9FAFBFCFDFEFF", + "6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c3710", + "9cd7481d3b7ca904b14b4084d9d4c83ed39eac8e16747895fc2ae1eecd220276af3d0d2f21cb3807561347c81ad138117dd85c652afe16a47dc68eb884068ae3", + }, + { + "2B7E151628AED2A6ABF7158809CF4F3C", + "000102030405060708090A0B0C0D0E0F", + "F0F1F2F3F4F5F6F7F8F9FAFBFCFDFEFF", + "6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417b", + "f7505aff357ac13107cdb2848c6bb2dcdda473f7a6ea939d44f52c986c11ca9341042f2b0091a1ca5c8f708cae8ca6a5c59e2228b3616c4455627722", + }, + { + "2B7E151628AED2A6ABF7158809CF4F3C", + "000102030405060708090A0B0C0D0E0F", + "F0F1F2F3F4F5F6F7F8F9FAFBFCFDFEFF", + "6bc1bee22e409f96e93d7e117393172a", + "b7b1dd75f608012dc69621d4ea720a60", + }, +} + +func TestHCTR(t *testing.T) { + for i, test := range hctrSM4TestVectors { + key1, _ := hex.DecodeString(test.key) + key2, _ := hex.DecodeString(test.hashKey) + tw, _ := hex.DecodeString(test.tweak) + plaintext, _ := hex.DecodeString(test.plaintext) + ciphertext, _ := hex.DecodeString(test.ciphertext) + got := make([]byte, len(plaintext)) + c, err := sm4.NewCipher(key1) + if err != nil { + t.Fatal(err) + } + hctr, err := cipher.NewHCTR(c, tw, key2) + if err != nil { + t.Fatal(err) + } + hctr.Encrypt(got, plaintext) + if !bytes.Equal(got, ciphertext) { + t.Fatalf("%v case encrypt failed, got %x\n", i+1, got) + } + + hctr.Decrypt(got, ciphertext) + if !bytes.Equal(got, plaintext) { + t.Fatalf("%v case decrypt failed, got %x\n", i+1, got) + } + } +} diff --git a/cipher/xts.go b/cipher/xts.go index 50fbeb7..22a9611 100644 --- a/cipher/xts.go +++ b/cipher/xts.go @@ -317,8 +317,10 @@ func (c *xtsDecrypter) CryptBlocks(plaintext, ciphertext []byte) { func mul2Generic(tweak *[blockSize]byte, isGB bool) { var carryIn byte if !isGB { - // tweak[0] represents the coefficients of {x^7, x^6, ..., x^0} - // tweak[15] represents the coefficients of {x^127, x^126, ..., x^120} + // the coefficient of x⁰ can be obtained by tweak[0] & 1 + // the coefficient of x⁷ can be obtained by tweak[0] >> 7 + // the coefficient of x¹²⁰ can be obtained by tweak[15] & 1 + // the coefficient of x¹²⁷ can be obtained by tweak[15] >> 7 for j := range tweak { carryOut := tweak[j] >> 7 tweak[j] = (tweak[j] << 1) + carryIn @@ -334,9 +336,11 @@ func mul2Generic(tweak *[blockSize]byte, isGB bool) { tweak[0] ^= GF128_FDBK // 1<<7 | 1<<2 | 1<<1 | 1 } } else { - // GB/T 17964-2021, because of the bit-ordering, doubling is actually a right shift. - // tweak[0] represents the coefficients of {x^0, x^1, ..., x^7} - // tweak[15] represents the coefficients of {x^120, x^121, ..., x^127} + // GB/T 17964-2021, + // the coefficient of x⁰ can be obtained by tweak[0] >> 7 + // the coefficient of x⁷ can be obtained by tweak[0] & 1 + // the coefficient of x¹²⁰ can be obtained by tweak[15] >> 7 + // the coefficient of x¹²⁷ can be obtained by tweak[15] & 1 for j := range tweak { carryOut := (tweak[j] << 7) & 0x80 tweak[j] = (tweak[j] >> 1) + carryIn diff --git a/internal/subtle/xor_amd64.s b/internal/subtle/xor_amd64.s index 9e57e81..f2dfa15 100644 --- a/internal/subtle/xor_amd64.s +++ b/internal/subtle/xor_amd64.s @@ -62,7 +62,7 @@ ret: RET avx2: - TESTQ $31, DX // AND 31 & len, if not zero jump to not_aligned. + TESTQ $31, DX // AND 31 & len, if not zero jump to avx2_not_aligned. JNZ avx2_not_aligned avx2_aligned: