From 2756894219036066e7e8d1fc6e8e4b037e256c14 Mon Sep 17 00:00:00 2001 From: omicron Date: Mon, 19 May 2025 02:11:38 +0200 Subject: [PATCH] Add Speck-128 implementation --- cipher/cipher.go | 20 ++++++ cipher/speck/impl/speck128.go | 130 ++++++++++++++++++++++++++++++++++ cipher/speck/speck.go | 64 +++++++++++++++++ cipher/speck/speck_test.go | 75 ++++++++++++++++++++ 4 files changed, 289 insertions(+) create mode 100644 cipher/cipher.go create mode 100644 cipher/speck/impl/speck128.go create mode 100644 cipher/speck/speck.go create mode 100644 cipher/speck/speck_test.go diff --git a/cipher/cipher.go b/cipher/cipher.go new file mode 100644 index 0000000..153364c --- /dev/null +++ b/cipher/cipher.go @@ -0,0 +1,20 @@ +package cipher + +import "errors" + +var ErrInvalidKeyLength = errors.New("Invalid key length") + +// A Block represents an implementation of a block cipher using block cipher +// specific parameters. +type Block interface { + // Encrypt a source block into the destination. dst and src must be exactly + // block sized. Panics if blocks are not sized correctly. + Encrypt(dst, src []byte) + // Decrypt a source block into the destination. dst and src must be exactly + // block sized. Panics if blocks are not sized correctly. + Decrypt(dst, src []byte) + // BlockSize returns the blocksize in bytes + BlockSize() int + // Algorithm returns the name of the algorithm + Algorithm() string +} diff --git a/cipher/speck/impl/speck128.go b/cipher/speck/impl/speck128.go new file mode 100644 index 0000000..25bdd93 --- /dev/null +++ b/cipher/speck/impl/speck128.go @@ -0,0 +1,130 @@ +// impl implements the Speck algorithm. This implementation should not be used +// and instead the parent package should be used. The implementations exposes +// all the internal details for testing and analysis. +package impl + +import ( + "encoding/binary" + "math/bits" + + "git.omicron.one/playground/cryptography/cipher" +) + +const ( + BlockSize128 = 128 / 8 + KeySize128128 = 128 / 8 + KeySize128192 = 192 / 8 + KeySize128256 = 256 / 8 + Rounds128128 = 32 + Rounds128192 = 33 + Rounds128256 = 34 +) + +type Speck128 struct { + Keys []uint64 +} + +func New128(key []byte) (*Speck128, error) { + var k [4]uint64 + var m int + var rounds int + + // Note that the layout of the k array is slightly different from the spec. + // Compared to the spec it is laid out like this: + // k[0] = l_0 + // k[1] = l_1 + // ... + // k[m] = k_0 + // + // This allows the key schedule loop to easily access (and overwrite) + // k[i % m] and k[m]. + + switch len(key) { + case KeySize128128: + rounds = Rounds128128 + m = 1 + k[0] = binary.BigEndian.Uint64(key[:8]) + k[1] = binary.BigEndian.Uint64(key[8:]) + case KeySize128192: + rounds = Rounds128192 + m = 2 + k[0] = binary.BigEndian.Uint64(key[8:]) + k[1] = binary.BigEndian.Uint64(key[:8]) + k[2] = binary.BigEndian.Uint64(key[16:24]) + case KeySize128256: + rounds = Rounds128256 + m = 3 + k[0] = binary.BigEndian.Uint64(key[16:24]) + k[1] = binary.BigEndian.Uint64(key[8:]) + k[2] = binary.BigEndian.Uint64(key[:8]) + k[3] = binary.BigEndian.Uint64(key[24:]) + default: + return nil, cipher.ErrInvalidKeyLength + } + + ctx := &Speck128{ + Keys: make([]uint64, rounds), + } + + ctx.Keys[0] = k[m] + for i := 0; i < rounds-1; i++ { + k[i%m], k[m] = Round128(uint64(i), k[i%m], k[m]) + ctx.Keys[i+1] = k[m] + } + return ctx, nil +} + +func Round128(k, x1, x2 uint64) (uint64, uint64) { + x1 = (bits.RotateLeft64(x1, 64-8) + x2) ^ k + x2 = bits.RotateLeft64(x2, 3) ^ x1 + return x1, x2 +} + +func InverseRound128(k, x1, x2 uint64) (uint64, uint64) { + x2 = bits.RotateLeft64(x2^x1, 64-3) + x1 = bits.RotateLeft64((x1^k)-x2, 8) + return x1, x2 +} + +func (ctx *Speck128) Encrypt(dst, src []byte) { + if len(dst) != BlockSize128 || len(src) != BlockSize128 { + panic("Incorrect blocksize, expected 128 bits") + } + + x1 := binary.BigEndian.Uint64(src[:8]) + x2 := binary.BigEndian.Uint64(src[8:]) + for _, k := range ctx.Keys { + x1, x2 = Round128(k, x1, x2) + } + binary.BigEndian.PutUint64(dst[:8], x1) + binary.BigEndian.PutUint64(dst[8:], x2) +} + +func (ctx *Speck128) Decrypt(dst, src []byte) { + if len(dst) != BlockSize128 || len(src) != BlockSize128 { + panic("Incorrect blocksize, expected 128 bits") + } + x1 := binary.BigEndian.Uint64(src[:8]) + x2 := binary.BigEndian.Uint64(src[8:]) + for i := len(ctx.Keys) - 1; i >= 0; i-- { + x1, x2 = InverseRound128(ctx.Keys[i], x1, x2) + } + binary.BigEndian.PutUint64(dst[:8], x1) + binary.BigEndian.PutUint64(dst[8:], x2) +} + +func (ctx *Speck128) BlockSize() int { + return BlockSize128 +} + +func (ctx *Speck128) Algorithm() string { + switch len(ctx.Keys) { + case Rounds128128: + return "Speck128/128" + case Rounds128192: + return "Speck128/192" + case Rounds128256: + return "Speck128/256" + } + panic("unreachable") +} diff --git a/cipher/speck/speck.go b/cipher/speck/speck.go new file mode 100644 index 0000000..5e462b9 --- /dev/null +++ b/cipher/speck/speck.go @@ -0,0 +1,64 @@ +// Package speck implements (parts of) the Speck block cipher as defined in +// https://eprint.iacr.org/2013/404.pdf. +package speck + +import ( + "fmt" + + "git.omicron.one/playground/cryptography/cipher" + "git.omicron.one/playground/cryptography/cipher/speck/impl" +) + +type SpeckParameters int + +const ( + Speck3264 = iota + 1 + Speck4872 + Speck4896 + Speck6496 + Speck64128 + Speck9696 + Speck96144 + Speck128128 + Speck128192 + Speck128256 +) + +var keySizes = []int{ + 0, // unused + 8, // Speck3264 + 9, // Speck4872 + 12, // Speck4896 + 12, // Speck6496 + 16, // Speck64128 + 12, // Speck9696 + 18, // Speck96144 + 16, // Speck128128 + 24, // Speck128192 + 32, // Speck128256 +} + +// New creates a new speck block cipher context. +// Returns the created block cipher or an error. +func New(key []byte, param SpeckParameters) (cipher.Block, error) { + if param == 0 || int(param) > len(keySizes) { + panic("Invalid parameters") + } + keySize := keySizes[param] + if len(key) != keySize { + return nil, cipher.ErrInvalidKeyLength + } + switch param { + case Speck3264: + return nil, fmt.Errorf("Not implemented") + case Speck4872, Speck4896: + return nil, fmt.Errorf("Not implemented") + case Speck6496, Speck64128: + return nil, fmt.Errorf("Not implemented") + case Speck9696, Speck96144: + return nil, fmt.Errorf("Not implemented") + case Speck128128, Speck128192, Speck128256: + return impl.New128(key) + } + panic("unreachable") +} diff --git a/cipher/speck/speck_test.go b/cipher/speck/speck_test.go new file mode 100644 index 0000000..fdc105c --- /dev/null +++ b/cipher/speck/speck_test.go @@ -0,0 +1,75 @@ +package speck_test + +import ( + "encoding/hex" + "slices" + "testing" + + "git.omicron.one/playground/cryptography/cipher/speck" + "github.com/stretchr/testify/assert" +) + +func DeHex(s string) []byte { + decoded, err := hex.DecodeString(s) + if err != nil { + panic("invalid hex string") + } + return decoded +} + +type TestVector struct { + Key []byte + Plaintext []byte + Ciphertext []byte + Param speck.SpeckParameters +} + +var vectors []TestVector = []TestVector{ + // Speck128/128 test vector + { + Key: DeHex("0f0e0d0c0b0a09080706050403020100"), + Plaintext: DeHex("6c617669757165207469206564616d20"), + Ciphertext: DeHex("a65d9851797832657860fedf5c570d18"), + Param: speck.Speck128128, + }, + { + Key: DeHex("17161514131211100f0e0d0c0b0a09080706050403020100"), + Plaintext: DeHex("726148206665696843206f7420746e65"), + Ciphertext: DeHex("1be4cf3a13135566f9bc185de03c1886"), + Param: speck.Speck128192, + }, + { + Key: DeHex("1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100"), + Plaintext: DeHex("65736f6874206e49202e72656e6f6f70"), + Ciphertext: DeHex("4109010405c0f53e4eeeb48d9c188f43"), + Param: speck.Speck128256, + }, +} + +func TestVectors(t *testing.T) { + for _, vector := range vectors { + ctx, err := speck.New(vector.Key, vector.Param) + assert.NotNil(t, ctx) + assert.Nil(t, err) + + // Test in place + buffer := slices.Clone(vector.Plaintext) + ctx.Encrypt(buffer, buffer) + assert.Equal(t, vector.Ciphertext, buffer, ctx.Algorithm()) + ctx.Decrypt(buffer, buffer) + assert.Equal(t, vector.Plaintext, buffer, ctx.Algorithm()) + + // Test two buffers + dst := make([]byte, len(vector.Ciphertext)) + src := slices.Clone(vector.Plaintext) + ctx.Encrypt(dst, src) + assert.Equal(t, vector.Plaintext, src, ctx.Algorithm()) + assert.Equal(t, vector.Ciphertext, dst, ctx.Algorithm()) + + dst = make([]byte, len(vector.Plaintext)) + src = slices.Clone(vector.Ciphertext) + ctx.Decrypt(dst, src) + assert.Equal(t, vector.Ciphertext, src, ctx.Algorithm()) + assert.Equal(t, vector.Plaintext, dst, ctx.Algorithm()) + } +} -- 2.49.0