diff --git a/cipher/ghash.go b/cipher/ghash.go new file mode 100644 index 0000000..5a65329 --- /dev/null +++ b/cipher/ghash.go @@ -0,0 +1,126 @@ +package cipher + +import "github.com/emmansun/gmsm/internal/byteorder" + +const ( + ghashBlockSize = 16 +) + +// ghashFieldElement represents a value in GF(2¹²⁸). In order to reflect the GCM +// standard and make binary.BigEndian suitable for marshaling these values, the +// bits are stored in big endian order. For example: +// +// the coefficient of x⁰ can be obtained by v.low >> 63. +// the coefficient of x⁶³ can be obtained by v.low & 1. +// the coefficient of x⁶⁴ can be obtained by v.high >> 63. +// the coefficient of x¹²⁷ can be obtained by v.high & 1. +type ghashFieldElement struct { + low, high uint64 +} + +// reverseBits reverses the order of the bits of 4-bit number in i. +func reverseBits(i int) int { + i = ((i << 2) & 0xc) | ((i >> 2) & 0x3) + i = ((i << 1) & 0xa) | ((i >> 1) & 0x5) + return i +} + +// hctrAdd adds two elements of GF(2¹²⁸) and returns the sum. +func ghashAdd(x, y *ghashFieldElement) ghashFieldElement { + // Addition in a characteristic 2 field is just XOR. + return ghashFieldElement{x.low ^ y.low, x.high ^ y.high} +} + +// hctrDouble returns the result of doubling an element of GF(2¹²⁸). +func ghashDouble(x *ghashFieldElement) (double ghashFieldElement) { + msbSet := x.high&1 == 1 + + // Because of the bit-ordering, doubling is actually a right shift. + double.high = x.high >> 1 + double.high |= x.low << 63 + double.low = x.low >> 1 + + // If the most-significant bit was set before shifting then it, + // conceptually, becomes a term of x^128. This is greater than the + // irreducible polynomial so the result has to be reduced. The + // irreducible polynomial is 1+x+x^2+x^7+x^128. We can subtract that to + // eliminate the term at x^128 which also means subtracting the other + // four terms. In characteristic 2 fields, subtraction == addition == + // XOR. + if msbSet { + double.low ^= 0xe100000000000000 + } + + return +} + +// ghashReductionTable is stored irreducible polynomial's double & add precomputed results. +// 0000 - 0 +// 0001 - irreducible polynomial >> 3 +// 0010 - irreducible polynomial >> 2 +// 0011 - (irreducible polynomial >> 3 xor irreducible polynomial >> 2) +// ... +// 1000 - just the irreducible polynomial +var ghashReductionTable = []uint16{ + 0x0000, 0x1c20, 0x3840, 0x2460, 0x7080, 0x6ca0, 0x48c0, 0x54e0, + 0xe100, 0xfd20, 0xd940, 0xc560, 0x9180, 0x8da0, 0xa9c0, 0xb5e0, +} + +// ghashMul sets y to y*H, where H is the GHASH key, fixed during New. +func ghashMul(productTable *[16]ghashFieldElement, y *ghashFieldElement) { + var z ghashFieldElement + + // Eliminate bounds checks in the loop. + _ = ghashReductionTable[0xf] + + for i := 0; i < 2; i++ { + word := y.high + if i == 1 { + word = y.low + } + + // Multiplication works by multiplying z by 16 and adding in + // one of the precomputed multiples of hash key. + for j := 0; j < 64; j += 4 { + msw := z.high & 0xf + z.high >>= 4 + z.high |= z.low << 60 + z.low >>= 4 + z.low ^= uint64(ghashReductionTable[msw]) << 48 + + // the values in |table| are ordered for + // little-endian bit positions. + t := &productTable[word&0xf] + + z.low ^= t.low + z.high ^= t.high + word >>= 4 + } + } + + *y = z +} + +// updateBlocks extends y with more polynomial terms from blocks, based on +// Horner's rule. There must be a multiple of gcmBlockSize bytes in blocks. +func updateBlocks(productTable *[16]ghashFieldElement, y *ghashFieldElement, blocks []byte) { + for len(blocks) > 0 { + y.low ^= byteorder.BEUint64(blocks) + y.high ^= byteorder.BEUint64(blocks[8:]) + ghashMul(productTable, y) + blocks = blocks[blockSize:] + } +} + +// ghashUpdate extends y with more polynomial terms from data. If data is not a +// multiple of gcmBlockSize bytes long then the remainder is zero padded. +func ghashUpdate(productTable *[16]ghashFieldElement, y *ghashFieldElement, data []byte) { + fullBlocks := (len(data) >> 4) << 4 + updateBlocks(productTable, y, data[:fullBlocks]) + + if len(data) != fullBlocks { + var partialBlock [blockSize]byte + copy(partialBlock[:], data[fullBlocks:]) + updateBlocks(productTable, y, partialBlock[:]) + } +} diff --git a/cipher/gxm.go b/cipher/gxm.go new file mode 100644 index 0000000..6ca2194 --- /dev/null +++ b/cipher/gxm.go @@ -0,0 +1,143 @@ +package cipher + +import ( + "crypto/cipher" + "crypto/subtle" + "errors" + + "github.com/emmansun/gmsm/internal/alias" + "github.com/emmansun/gmsm/internal/byteorder" +) + +type gxm struct { + stream cipher.Stream + tagSize int + tagMask [ghashBlockSize]byte + // productTable contains the first sixteen powers of the hash key. + // However, they are in bit reversed order. + productTable [16]ghashFieldElement +} + +// NewGXM creates a new GXM instance using the provided cipher stream and hash key. +// It uses the default tag size of 16 bytes. +func NewGXM(stream cipher.Stream, hkey []byte) (*gxm, error) { + return NewGXMWithTagSize(stream, hkey, 16) +} + +// NewGXMWithTagSize creates a new instance of GXM (Galois XOR Mode) with a specified tag size. +func NewGXMWithTagSize(stream cipher.Stream, hkey []byte, tagSize int) (*gxm, error) { + if len(hkey) != ghashBlockSize { + return nil, errors.New("cipher: invalid hash key length") + } + if tagSize < 8 || tagSize > 16 { + return nil, errors.New("cipher: invalid tag size") + } + c := &gxm{} + c.stream = stream + c.tagSize = tagSize + // We precompute 16 multiples of |key|. However, when we do lookups + // into this table we'll be using bits from a field element and + // therefore the bits will be in the reverse order. So normally one + // would expect, say, 4*key to be in index 4 of the table but due to + // this bit ordering it will actually be in index 0010 (base 2) = 2. + x := ghashFieldElement{ + byteorder.BEUint64(hkey[:8]), + byteorder.BEUint64(hkey[8:blockSize]), + } + c.productTable[reverseBits(1)] = x + + for i := 2; i < 16; i += 2 { + c.productTable[reverseBits(i)] = ghashDouble(&c.productTable[reverseBits(i/2)]) + c.productTable[reverseBits(i+1)] = ghashAdd(&c.productTable[reverseBits(i)], &x) + } + + // encrypt zero block to get the tag mask + stream.XORKeyStream(c.tagMask[:tagSize], c.tagMask[:tagSize]) + + return c, nil +} + +// Overhead returns the maximum difference between the lengths of a +// plaintext and its ciphertext. +func (g *gxm) Overhead() int { + return g.tagSize +} + +// Seal encrypts and authenticates plaintext, authenticates the +// additional data and appends the result to dst, returning the updated +// slice. The nonce must be NonceSize() bytes long and unique for all +// time, for a given key. +// +// To reuse plaintext's storage for the encrypted output, use plaintext[:0] +// as dst. Otherwise, the remaining capacity of dst must not overlap plaintext. +// dst and additionalData may not overlap. +func (g *gxm) Seal(dst, plaintext, additionalData []byte) []byte { + ret, out := alias.SliceForAppend(dst, len(plaintext)+g.tagSize) + if alias.InexactOverlap(out, plaintext) { + panic("cipher: invalid buffer overlap of output and input") + } + if alias.AnyOverlap(out, additionalData) { + panic("cipher: invalid buffer overlap of output and additional data") + } + + g.stream.XORKeyStream(out, plaintext) + g.gxmAuth(out[len(plaintext):], out[:len(plaintext)], additionalData) + return ret +} + +// Open decrypts and authenticates ciphertext, authenticates the +// additional data and, if successful, appends the resulting plaintext +// to dst, returning the updated slice. The nonce must be NonceSize() +// bytes long and both it and the additional data must match the +// value passed to Seal. +// +// To reuse ciphertext's storage for the decrypted output, use ciphertext[:0] +// as dst. Otherwise, the remaining capacity of dst must not overlap ciphertext. +// dst and additionalData may not overlap. +// +// Even if the function fails, the contents of dst, up to its capacity, +// may be overwritten. +func (g *gxm) Open(dst, ciphertext, additionalData []byte) ([]byte, error) { + if len(ciphertext) < g.tagSize { + return nil, errOpen + } + ret, out := alias.SliceForAppend(dst, len(ciphertext)-g.tagSize) + if alias.InexactOverlap(out, ciphertext) { + panic("cipher: invalid buffer overlap of output and input") + } + if alias.AnyOverlap(out, additionalData) { + panic("cipher: invalid buffer overlap of output and additional data") + } + tag := ciphertext[len(ciphertext)-g.tagSize:] + ciphertext = ciphertext[:len(ciphertext)-g.tagSize] + + var expectedTag [blockSize]byte + g.gxmAuth(expectedTag[:], ciphertext, additionalData) + + // Use subtle.ConstantTimeCompare to avoid leaking timing information. + if subtle.ConstantTimeCompare(expectedTag[:g.tagSize], tag) != 1 { + // We sometimes decrypt and authenticate concurrently, so we overwrite + // dst in the event of a tag mismatch. To be consistent across platforms + // and to avoid releasing unauthenticated plaintext, we clear the buffer + // in the event of an error. + clear(out) + return nil, errOpen + } + g.stream.XORKeyStream(out, ciphertext) + return ret, nil +} + +func (g *gxm) gxmAuth(out, ciphertext, additionalData []byte) { + var tag [ghashBlockSize]byte + tagField := ghashFieldElement{} + ghashUpdate(&g.productTable, &tagField, additionalData) + ghashUpdate(&g.productTable, &tagField, ciphertext) + lenBlock := make([]byte, 16) + byteorder.BEPutUint64(lenBlock[:8], uint64(len(additionalData))*8) + byteorder.BEPutUint64(lenBlock[8:], uint64(len(ciphertext))*8) + ghashUpdate(&g.productTable, &tagField, lenBlock) + byteorder.BEPutUint64(tag[:], tagField.low) + byteorder.BEPutUint64(tag[8:], tagField.high) + subtle.XORBytes(tag[:], tag[:], g.tagMask[:]) + copy(out, tag[:g.tagSize]) +} diff --git a/cipher/hctr.go b/cipher/hctr.go index 8d2fc0b..403bde8 100644 --- a/cipher/hctr.go +++ b/cipher/hctr.go @@ -40,66 +40,6 @@ type LengthPreservingMode interface { BlockSize() int } -// hctrFieldElement represents a value in GF(2¹²⁸). In order to reflect the HCTR -// standard and make binary.BigEndian suitable for marshaling these values, the -// bits are stored in big endian order. For example: -// -// the coefficient of x⁰ can be obtained by v.low >> 63. -// the coefficient of x⁶³ can be obtained by v.low & 1. -// the coefficient of x⁶⁴ can be obtained by v.high >> 63. -// the coefficient of x¹²⁷ can be obtained by v.high & 1. -type hctrFieldElement struct { - low, high uint64 -} - -// reverseBits reverses the order of the bits of 4-bit number in i. -func reverseBits(i int) int { - i = ((i << 2) & 0xc) | ((i >> 2) & 0x3) - i = ((i << 1) & 0xa) | ((i >> 1) & 0x5) - return i -} - -// hctrAdd adds two elements of GF(2¹²⁸) and returns the sum. -func hctrAdd(x, y *hctrFieldElement) hctrFieldElement { - // Addition in a characteristic 2 field is just XOR. - return hctrFieldElement{x.low ^ y.low, x.high ^ y.high} -} - -// hctrDouble returns the result of doubling an element of GF(2¹²⁸). -func hctrDouble(x *hctrFieldElement) (double hctrFieldElement) { - msbSet := x.high&1 == 1 - - // Because of the bit-ordering, doubling is actually a right shift. - double.high = x.high >> 1 - double.high |= x.low << 63 - double.low = x.low >> 1 - - // If the most-significant bit was set before shifting then it, - // conceptually, becomes a term of x^128. This is greater than the - // irreducible polynomial so the result has to be reduced. The - // irreducible polynomial is 1+x+x^2+x^7+x^128. We can subtract that to - // eliminate the term at x^128 which also means subtracting the other - // four terms. In characteristic 2 fields, subtraction == addition == - // XOR. - if msbSet { - double.low ^= 0xe100000000000000 - } - - return -} - -// hctrReductionTable is stored irreducible polynomial's double & add precomputed results. -// 0000 - 0 -// 0001 - irreducible polynomial >> 3 -// 0010 - irreducible polynomial >> 2 -// 0011 - (irreducible polynomial >> 3 xor irreducible polynomial >> 2) -// ... -// 1000 - just the irreducible polynomial -var hctrReductionTable = []uint16{ - 0x0000, 0x1c20, 0x3840, 0x2460, 0x7080, 0x6ca0, 0x48c0, 0x54e0, - 0xe100, 0xfd20, 0xd940, 0xc560, 0x9180, 0x8da0, 0xa9c0, 0xb5e0, -} - // hctr represents a Variable-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 @@ -109,7 +49,7 @@ type hctr struct { tweak [blockSize]byte // productTable contains the first sixteen powers of the hash key. // However, they are in bit reversed order. - productTable [16]hctrFieldElement + productTable [16]ghashFieldElement } func (h *hctr) BlockSize() int { @@ -130,56 +70,25 @@ func NewHCTR(cipher cipher.Block, tweak, hkey []byte) (LengthPreservingMode, err // therefore the bits will be in the reverse order. So normally one // would expect, say, 4*key to be in index 4 of the table but due to // this bit ordering it will actually be in index 0010 (base 2) = 2. - x := hctrFieldElement{ + x := ghashFieldElement{ byteorder.BEUint64(hkey[:8]), byteorder.BEUint64(hkey[8:blockSize]), } c.productTable[reverseBits(1)] = x for i := 2; i < 16; i += 2 { - c.productTable[reverseBits(i)] = hctrDouble(&c.productTable[reverseBits(i/2)]) - c.productTable[reverseBits(i+1)] = hctrAdd(&c.productTable[reverseBits(i)], &x) + c.productTable[reverseBits(i)] = ghashDouble(&c.productTable[reverseBits(i/2)]) + c.productTable[reverseBits(i+1)] = ghashAdd(&c.productTable[reverseBits(i)], &x) } return c, nil } // mul sets y to y*H, where H is the GCM key, fixed during NewHCTR. -func (h *hctr) mul(y *hctrFieldElement) { - var z hctrFieldElement - - // Eliminate bounds checks in the loop. - _ = hctrReductionTable[0xf] - - for i := 0; i < 2; i++ { - word := y.high - if i == 1 { - word = y.low - } - - // Multiplication works by multiplying z by 16 and adding in - // one of the precomputed multiples of hash key. - for j := 0; j < 64; j += 4 { - msw := z.high & 0xf - z.high >>= 4 - z.high |= z.low << 60 - z.low >>= 4 - z.low ^= uint64(hctrReductionTable[msw]) << 48 - - // the values in |table| are ordered for - // little-endian bit positions. See the comment - // in NewHCTR. - t := &h.productTable[word&0xf] - - z.low ^= t.low - z.high ^= t.high - word >>= 4 - } - } - - *y = z +func (h *hctr) mul(y *ghashFieldElement) { + ghashMul(&h.productTable, y) } -func (h *hctr) updateBlock(block []byte, y *hctrFieldElement) { +func (h *hctr) updateBlock(block []byte, y *ghashFieldElement) { y.low ^= byteorder.BEUint64(block) y.high ^= byteorder.BEUint64(block[8:]) h.mul(y) @@ -188,7 +97,7 @@ func (h *hctr) updateBlock(block []byte, y *hctrFieldElement) { // 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, out *[blockSize]byte) { - var y hctrFieldElement + var y ghashFieldElement msg := m // update blocks for len(msg) >= blockSize { diff --git a/cipher/mur.go b/cipher/mur.go new file mode 100644 index 0000000..f986c29 --- /dev/null +++ b/cipher/mur.go @@ -0,0 +1,185 @@ +package cipher + +import ( + "crypto/cipher" + "crypto/subtle" + "errors" + + "github.com/emmansun/gmsm/internal/alias" + "github.com/emmansun/gmsm/internal/byteorder" +) + +type StreamCipherCreator func(key, iv []byte) (cipher.Stream, error) + +const ( + maxIVSize = 32 + maxTagSize = 16 +) + +type mur struct { + streamCipherCreator StreamCipherCreator + + tagSize int + // productTable contains the first sixteen powers of the hash key. + // However, they are in bit reversed order. + productTable [16]ghashFieldElement +} + +// NewMUR creates a new MUR (misuse-resistant AEAD mode) instance with a default tag size of 16 bytes. +// It takes a StreamCipherCreator function for generating the underlying stream cipher and an ghash key. +func NewMUR(streamCipherCreator StreamCipherCreator, hkey []byte) (*mur, error) { + return NewMURWithTagSize(streamCipherCreator, hkey, 16) +} + +// NewMURWithTagSize creates a new MUR (misuse-resistant AEAD mode) instance with the specified tag size. +func NewMURWithTagSize(streamCipherCreator StreamCipherCreator, hkey []byte, tagSize int) (*mur, error) { + if len(hkey) != ghashBlockSize { + return nil, errors.New("cipher: invalid hash key length") + } + if tagSize < 8 || tagSize > 16 { + return nil, errors.New("cipher: invalid tag size") + } + + c := &mur{} + c.streamCipherCreator = streamCipherCreator + c.tagSize = tagSize + // We precompute 16 multiples of |key|. However, when we do lookups + // into this table we'll be using bits from a field element and + // therefore the bits will be in the reverse order. So normally one + // would expect, say, 4*key to be in index 4 of the table but due to + // this bit ordering it will actually be in index 0010 (base 2) = 2. + x := ghashFieldElement{ + byteorder.BEUint64(hkey[:8]), + byteorder.BEUint64(hkey[8:ghashBlockSize]), + } + c.productTable[reverseBits(1)] = x + + for i := 2; i < 16; i += 2 { + c.productTable[reverseBits(i)] = ghashDouble(&c.productTable[reverseBits(i/2)]) + c.productTable[reverseBits(i+1)] = ghashAdd(&c.productTable[reverseBits(i)], &x) + } + + return c, nil +} + +// Overhead returns the maximum difference between the lengths of a +// plaintext and its ciphertext. +func (g *mur) Overhead() int { + return g.tagSize +} + +// Seal encrypts and authenticates plaintext, authenticates the +// additional data and appends the result to dst, returning the updated +// slice. The nonce must be NonceSize() bytes long and unique for all +// time, for a given key. +// +// To reuse plaintext's storage for the encrypted output, use plaintext[:0] +// as dst. Otherwise, the remaining capacity of dst must not overlap plaintext. +// dst and additionalData may not overlap. +func (g *mur) Seal(iv, key1, key2, dst, plaintext, additionalData []byte) ([]byte, error) { + ret, out := alias.SliceForAppend(dst, len(plaintext)+g.tagSize) + if alias.InexactOverlap(out, plaintext) { + panic("cipher: invalid buffer overlap") + } + + var ( + tmpIV [maxIVSize]byte + tag [maxTagSize]byte + ivLen = len(iv) + ) + + if ivLen > maxIVSize { + panic("cipher: iv too large") + } + + copy(tmpIV[:], iv) + g.murAuth(tmpIV[:], plaintext, additionalData) + subtle.XORBytes(tmpIV[:], tmpIV[:], iv) + tagStream, err := g.streamCipherCreator(key2, tmpIV[:ivLen]) + if err != nil { + return nil, err + } + tagStream.XORKeyStream(tag[:g.tagSize], tag[:g.tagSize]) + + clear(tmpIV[:]) + subtle.XORBytes(tmpIV[:], iv, tag[:]) + dataStream, err := g.streamCipherCreator(key1, tmpIV[:ivLen]) + if err != nil { + return nil, err + } + dataStream.XORKeyStream(out, plaintext) + copy(out[len(plaintext):], tag[:g.tagSize]) + return ret, nil +} + +// Open decrypts and authenticates ciphertext, authenticates the +// additional data and, if successful, appends the resulting plaintext +// to dst, returning the updated slice. The nonce must be NonceSize() +// bytes long and both it and the additional data must match the +// value passed to Seal. +// +// To reuse ciphertext's storage for the decrypted output, use ciphertext[:0] +// as dst. Otherwise, the remaining capacity of dst must not overlap ciphertext. +// dst and additionalData may not overlap. +// +// Even if the function fails, the contents of dst, up to its capacity, +// may be overwritten. +func (g *mur) Open(iv, key1, key2, dst, ciphertext, additionalData []byte) ([]byte, error) { + if len(ciphertext) < g.tagSize { + return nil, errOpen + } + ret, out := alias.SliceForAppend(dst, len(ciphertext)-g.tagSize) + if alias.InexactOverlap(out, ciphertext) { + panic("cipher: invalid buffer overlap of output and input") + } + if alias.AnyOverlap(out, additionalData) { + panic("cipher: invalid buffer overlap of output and additional data") + } + tag := ciphertext[len(ciphertext)-g.tagSize:] + ciphertext = ciphertext[:len(ciphertext)-g.tagSize] + + var ( + tmpIV [maxIVSize]byte + calTag [maxTagSize]byte + ivLen = len(iv) + ) + if ivLen > maxIVSize { + panic("cipher: iv too large") + } + copy(tmpIV[:], tag) + subtle.XORBytes(tmpIV[:], iv, tmpIV[:]) + dataStream, err := g.streamCipherCreator(key1, tmpIV[:ivLen]) + if err != nil { + return nil, err + } + dataStream.XORKeyStream(out, ciphertext) + + clear(tmpIV[:]) + g.murAuth(tmpIV[:], out, additionalData) + subtle.XORBytes(tmpIV[:], tmpIV[:], iv) + tagStream, err := g.streamCipherCreator(key2, tmpIV[:ivLen]) + if err != nil { + return nil, err + } + tagStream.XORKeyStream(calTag[:g.tagSize], calTag[:g.tagSize]) + + if subtle.ConstantTimeCompare(tag, calTag[:g.tagSize]) != 1 { + clear(out) + return nil, errOpen + } + return ret, nil +} + +func (g *mur) murAuth(out []byte, plaintext, additionalData []byte) { + var tag [ghashBlockSize]byte + tagField := ghashFieldElement{} + ghashUpdate(&g.productTable, &tagField, additionalData) + ghashUpdate(&g.productTable, &tagField, plaintext) + lenBlock := make([]byte, 16) + byteorder.BEPutUint64(lenBlock[:8], uint64(len(additionalData))*8) + byteorder.BEPutUint64(lenBlock[8:], uint64(len(plaintext))*8) + ghashUpdate(&g.productTable, &tagField, lenBlock) + byteorder.BEPutUint64(tag[:], tagField.low) + byteorder.BEPutUint64(tag[8:], tagField.high) + copy(out, tag[:]) +} diff --git a/cipher/zuc_gxm_test.go b/cipher/zuc_gxm_test.go new file mode 100644 index 0000000..6e16e10 --- /dev/null +++ b/cipher/zuc_gxm_test.go @@ -0,0 +1,118 @@ +package cipher_test + +import ( + "bytes" + "encoding/hex" + "testing" + + "github.com/emmansun/gmsm/cipher" + "github.com/emmansun/gmsm/zuc" +) + +// GM/T 0001.4 - 2024 Appendix C.2 +var gxmTestCases = []struct { + iv string + h string + k string + a string + p string + result string + tagSize int +}{ + { + iv: "b3a6db3c870c3e99245e0d1c06b747de", + h: "6db45e4f9572f4e6fe0d91acda6801d5", + k: "edbe06afed8075576aad04afdec91d32", + a: "9de18b1fdab0ca9902b9729d492c807ec599d5", + p: "", + result: "2a14afaeb6e5ecc784fad24ddeb457d2", + tagSize: 16, + }, + { + iv: "2923be84e16cd6ae529049f1f1bbe9eb", + h: "27bede74018082da87d4e5b69f18bf66", + k: "32070e0f39b7b692b4673edc3184a48e", + a: "", + p: "", + result: "5d8a045ac89a681a4bc910380bbadccf", + tagSize: 16, + }, + { + iv: "2d2086832cc2fe3fd18cb51d6c5e99a5", + h: "9d6cb51623fd847f2e45d7f52f900db8", + k: "56131c03e457f6226b5477633b873984", + a: "", + p: "ffffffffffffffffffffffffffffff", + result: "b78e2f30cf70252d58767997f1b086efb30febbfe0c88a1e77b1dde9d45525", + tagSize: 16, + }, + { + iv: "bb8b76cfe5f0d9335029008b2a3b2b21", + h: "ee767d503bb3d5d1b585f57a0418c673", + k: "e4b5c1f8578034ce6424f58c675597ac", + a: "fcdd4cb97995da30efd957194eac4d2a8610470f99c88657f462f68dff7561a5", + p: "5fee5517627f17b22a96caf97b77ec7f667cc47d13c34923be2441300066a6c150b24d66c947ca7b2e708eb62bb352", + result: "b56da5c99238b04a45e3d9d96f12f3dc052e428fa5a5817292ee23dbdad9782cf66f55c846e55dc68f47eaf8378e7051c7aedd9e1c7d74c38059f5e7e3a742", + tagSize: 16, + }, + { + iv: "3615df810cc677f15080faa1dd44aad3", + h: "fdfaddc476785c25906fe42ba63a93b7", + k: "f405d652b6362e70f8362bd383b7298b", + a: "5fee5517627f17b22a96caf97b77ec7f667cc47d13c34923be2441300066a6c150b24d66c947ca7b2e708eb62bb352fc", + p: "dd4cb97995da30efd957194eac4d2a8610470f99c88657f462f68dff7561a5f3", + result: "1134ffc119ad163e914989474be6c072fd5867f3989d8b15899ebd10a4a248c98829aaa4f9891822", + tagSize: 8, + }, +} + +func TestGXMSeal(t *testing.T) { + for i, tc := range gxmTestCases { + key, _ := hex.DecodeString(tc.k) + iv, _ := hex.DecodeString(tc.iv) + h, _ := hex.DecodeString(tc.h) + a, _ := hex.DecodeString(tc.a) + p, _ := hex.DecodeString(tc.p) + expected, _ := hex.DecodeString(tc.result) + + eea, err := zuc.NewCipher(key, iv) + if err != nil { + t.Fatalf("case %d: NewCipher error: %s", i, err) + } + c, err := cipher.NewGXMWithTagSize(eea, h, tc.tagSize) + if err != nil { + t.Fatalf("case %d: NewGXM error: %s", i, err) + } + out := c.Seal(nil, p, a) + if !bytes.Equal(out, expected) { + t.Errorf("case %d: incorrect ciphertext\n got: %x\nwant: %x", i, out, expected) + } + } +} + +func TestGXMOpen(t *testing.T) { + for i, tc := range gxmTestCases { + key, _ := hex.DecodeString(tc.k) + iv, _ := hex.DecodeString(tc.iv) + h, _ := hex.DecodeString(tc.h) + a, _ := hex.DecodeString(tc.a) + p, _ := hex.DecodeString(tc.p) + expected, _ := hex.DecodeString(tc.result) + + eea, err := zuc.NewCipher(key, iv) + if err != nil { + t.Fatalf("case %d: NewCipher error: %s", i, err) + } + c, err := cipher.NewGXMWithTagSize(eea, h, tc.tagSize) + if err != nil { + t.Fatalf("case %d: NewGXM error: %s", i, err) + } + out, err := c.Open(nil, expected, a) + if err != nil { + t.Fatalf("case %d: Open error: %s", i, err) + } + if !bytes.Equal(out, p) { + t.Errorf("case %d: incorrect plaintext\n got: %x\nwant: %x", i, out, p) + } + } +} diff --git a/cipher/zuc_mur_test.go b/cipher/zuc_mur_test.go new file mode 100644 index 0000000..f88ce98 --- /dev/null +++ b/cipher/zuc_mur_test.go @@ -0,0 +1,134 @@ +package cipher_test + +import ( + "bytes" + _cipher "crypto/cipher" + "encoding/hex" + "testing" + + "github.com/emmansun/gmsm/cipher" + "github.com/emmansun/gmsm/zuc" +) + +var murTestCases = []struct { + iv string + h string + k1 string + k2 string + a string + p string + result string + tagSize int +}{ + // GM/T 0001.4 - 2024 Appendix C.3 + { + iv: "bb8b76cfe5f0d9335029008b2a3b2b21", + h: "ee767d503bb3d5d1b585f57a0418c673", + k1: "e4b5c1f8578034ce6424f58c675597ac", + k2: "608053f6af9efda562d95dc013bea6b5", + a: "fcdd4cb97995da30efd957194eac4d2a8610470f99c88657f462f68dff7561a5", + p: "5fee5517627f17b22a96caf97b77ec7f667cc47d13c34923be2441300066a6c150b24d66c947ca7b2e708eb62bb352", + result: "cf5594bd30c0da0fb41fa6054e534d0494c9d6c4f132fc85771a473458b09583b825c662bfd82278178a845e281e5415c5d1a78a42c4dcd67db05fa1a640a0", + tagSize: 16, + }, + { + iv: "2923be84e16cd6ae529049f1f1bbe9eb", + h: "27bede74018082da87d4e5b69f18bf66", + k1: "32070e0f39b7b692b4673edc3184a48e", + k2: "27636f4414510d62cc15cfe194ec4f6d", + a: "", + p: "", + result: "c0016e0772c9983d0fd9fd8c1b012845", + tagSize: 16, + }, + { + iv: "2d2086832cc2fe3fd18cb51d6c5e99a5", + h: "9d6cb51623fd847f2e45d7f52f900db8", + k1: "56131c03e457f6226b5477633b873984", + k2: "a88981534db331a386de3e52fb46029b", + a: "", + p: "ffffffffffffffffffffffffffffff", + result: "234c2d51eaa582da9be3cc3828aa670a7afb7d817efa0777826f1e33a53cf3", + tagSize: 16, + }, + { + iv: "b3a6db3c870c3e99245e0d1c06b747de", + h: "6db45e4f9572f4e6fe0d91acda6801d5", + k1: "edbe06afed8075576aad04afdec91d32", + k2: "61d4fca6b2c2bb48b4b1172531333620", + a: "9de18b1fdab0ca9902b9729d492c807ec599d5", + p: "", + result: "8213c29606d02bba10f13ffad1d26a42", + tagSize: 16, + }, + { + iv: "b3a6db3c870c3e99245e0d1c06b747de", + h: "6db45e4f9572f4e6fe0d91acda6801d5", + k1: "edbe06afed8075576aad04afdec91d32", + k2: "61d4fca6b2c2bb48b4b1172531333620", + a: "9de18b1fdab0ca9902b9729d492c807ec599d5e980b2eac9cc53bf67d6bf14d67e2ddc8e6683ef574961ff698f61cdd1", + p: "b3124dc843bb8ba61f035a7d0938251f5dd4cbfc96f5453b130d890a1cdbae32", + result: "dabbbe23d8f0ea42e31a9bdd9706a4275d8aacd2cf27c4a4c0d0ba6fb8f31da7a276827b74509357", + tagSize: 8, + }, +} + +func TestMurSeal(t *testing.T) { + zucCipherCreator := func(key, iv []byte) (_cipher.Stream, error) { + return zuc.NewCipher(key, iv) + } + for i, tc := range murTestCases { + iv, _ := hex.DecodeString(tc.iv) + h, _ := hex.DecodeString(tc.h) + k1, _ := hex.DecodeString(tc.k1) + k2, _ := hex.DecodeString(tc.k2) + a, _ := hex.DecodeString(tc.a) + p, _ := hex.DecodeString(tc.p) + result, _ := hex.DecodeString(tc.result) + + g, err := cipher.NewMURWithTagSize(zucCipherCreator, h, tc.tagSize) + if err != nil { + t.Errorf("case %d: NewMURWithTagSize error: %s", i, err) + continue + } + c, err := g.Seal(iv, k1, k2, nil, p, a) + if err != nil { + t.Errorf("case %d: Seal error: %s", i, err) + continue + } + if !bytes.Equal(c, result) { + t.Errorf("case %d: Seal mismatch\ngot: %x\nwant: %x", i, c, result) + continue + } + } +} + +func TestMurOpen(t *testing.T) { + zucCipherCreator := func(key, iv []byte) (_cipher.Stream, error) { + return zuc.NewCipher(key, iv) + } + for i, tc := range murTestCases { + iv, _ := hex.DecodeString(tc.iv) + h, _ := hex.DecodeString(tc.h) + k1, _ := hex.DecodeString(tc.k1) + k2, _ := hex.DecodeString(tc.k2) + a, _ := hex.DecodeString(tc.a) + p, _ := hex.DecodeString(tc.p) + result, _ := hex.DecodeString(tc.result) + + g, err := cipher.NewMURWithTagSize(zucCipherCreator, h, tc.tagSize) + if err != nil { + t.Errorf("case %d: NewMURWithTagSize error: %s", i, err) + continue + } + out, err := g.Open(iv, k1, k2, nil, result, a) + if err != nil { + t.Errorf("case %d: Open error: %s", i, err) + continue + } + if !bytes.Equal(out, p) { + t.Errorf("case %d: Open mismatch\ngot: %x\nwant: %x", i, out, p) + continue + } + } +}