cipher: initial support gxm & mur modes

This commit is contained in:
Sun Yimin 2025-09-26 15:44:43 +08:00 committed by GitHub
parent 570040e243
commit 00a09d0208
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 714 additions and 99 deletions

126
cipher/ghash.go Normal file
View File

@ -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[:])
}
}

143
cipher/gxm.go Normal file
View File

@ -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])
}

View File

@ -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 {

185
cipher/mur.go Normal file
View File

@ -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[:])
}

118
cipher/zuc_gxm_test.go Normal file
View File

@ -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)
}
}
}

134
cipher/zuc_mur_test.go Normal file
View File

@ -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
}
}
}