Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for RC4 #117

Merged
merged 2 commits into from
Oct 12, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cgo_go122.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,7 @@ package openssl
// functions that are known to allocate.
#cgo noescape go_openssl_EVP_PKEY_derive
#cgo nocallback go_openssl_EVP_PKEY_derive
#cgo noescape go_openssl_EVP_EncryptUpdate
#cgo nocallback go_openssl_EVP_EncryptUpdate
*/
import "C"
30 changes: 26 additions & 4 deletions cipher.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const (
cipherAES256
cipherDES
cipherDES3
cipherRC4
)

func (c cipherKind) String() string {
Expand All @@ -36,6 +37,8 @@ func (c cipherKind) String() string {
return "DES"
case cipherDES3:
return "DES3"
case cipherRC4:
return "RC4"
default:
panic("unknown cipher kind: " + strconv.Itoa(int(c)))
}
Expand All @@ -44,7 +47,8 @@ func (c cipherKind) String() string {
type cipherMode int8

const (
cipherModeECB cipherMode = iota
cipherModeNone cipherMode = -1
cipherModeECB cipherMode = iota
cipherModeCBC
cipherModeCTR
cipherModeGCM
Expand Down Expand Up @@ -133,6 +137,8 @@ func loadCipher(k cipherKind, mode cipherMode) (cipher C.GO_EVP_CIPHER_PTR) {
case cipherModeCBC:
cipher = C.go_openssl_EVP_des_ede3_cbc()
}
case cipherRC4:
cipher = C.go_openssl_EVP_rc4()
}
return cipher
}
Expand Down Expand Up @@ -477,17 +483,33 @@ func sliceForAppend(in []byte, n int) (head, tail []byte) {
return
}

func newCipherCtx(kind cipherKind, mode cipherMode, encrypt cipherOp, key, iv []byte) (C.GO_EVP_CIPHER_CTX_PTR, error) {
func newCipherCtx(kind cipherKind, mode cipherMode, encrypt cipherOp, key, iv []byte) (ctx C.GO_EVP_CIPHER_CTX_PTR, err error) {
cipher := loadCipher(kind, mode)
if cipher == nil {
panic("crypto/cipher: unsupported cipher: " + kind.String())
}
ctx := C.go_openssl_EVP_CIPHER_CTX_new()
ctx = C.go_openssl_EVP_CIPHER_CTX_new()
if ctx == nil {
return nil, fail("unable to create EVP cipher ctx")
}
defer func() {
if err != nil {
C.go_openssl_EVP_CIPHER_CTX_free(ctx)
}
}()
if kind == cipherRC4 {
// RC4 cipher supports a variable key length.
// We need to set the key length before setting the key,
// and to do so we need to have an initialized cipher ctx.
if C.go_openssl_EVP_CipherInit_ex(ctx, cipher, nil, nil, nil, C.int(encrypt)) != 1 {
return nil, newOpenSSLError("EVP_CipherInit_ex")
}
if C.go_openssl_EVP_CIPHER_CTX_set_key_length(ctx, C.int(len(key))) != 1 {
return nil, newOpenSSLError("EVP_CIPHER_CTX_set_key_length")
}
cipher = nil // don't reset the cipher
qmuntal marked this conversation as resolved.
Show resolved Hide resolved
}
if C.go_openssl_EVP_CipherInit_ex(ctx, cipher, nil, base(key), base(iv), C.int(encrypt)) != 1 {
C.go_openssl_EVP_CIPHER_CTX_free(ctx)
return nil, fail("unable to initialize EVP cipher ctx")
}
return ctx, nil
Expand Down
63 changes: 63 additions & 0 deletions rc4.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
//go:build !cmd_go_bootstrap

package openssl

// #include "goopenssl.h"
import "C"
import "runtime"

// SupportsRC4 returns true if NewRC4Cipher is supported.
func SupportsRC4() bool {
// True for stock OpenSSL 1.
// False for stock OpenSSL 3 unless the legacy provider is available.
return loadCipher(cipherRC4, cipherModeNone) != nil
}

// A RC4Cipher is an instance of RC4 using a particular key.
type RC4Cipher struct {
ctx C.GO_EVP_CIPHER_CTX_PTR
}

// NewRC4Cipher creates and returns a new Cipher.
func NewRC4Cipher(key []byte) (*RC4Cipher, error) {
ctx, err := newCipherCtx(cipherRC4, cipherModeNone, cipherOpEncrypt, key, nil)
if err != nil {
return nil, err
}
c := &RC4Cipher{ctx}
runtime.SetFinalizer(c, (*RC4Cipher).finalize)
return c, nil
}

func (c *RC4Cipher) finalize() {
if c.ctx != nil {
C.go_openssl_EVP_CIPHER_CTX_free(c.ctx)
}
}

// Reset zeros the key data and makes the Cipher unusable.
func (c *RC4Cipher) Reset() {
if c.ctx != nil {
C.go_openssl_EVP_CIPHER_CTX_free(c.ctx)
c.ctx = nil
}
}

// XORKeyStream sets dst to the result of XORing src with the key stream.
// Dst and src must overlap entirely or not at all.
func (c *RC4Cipher) XORKeyStream(dst, src []byte) {
if c.ctx == nil || len(src) == 0 {
return
}
if inexactOverlap(dst[:len(src)], src) {
panic("crypto/rc4: invalid buffer overlap")
}
var outLen C.int
if C.go_openssl_EVP_EncryptUpdate(c.ctx, base(dst), &outLen, base(src), C.int(len(src))) != 1 {
panic("crypto/cipher: EncryptUpdate failed")
}
if int(outLen) != len(src) {
panic("crypto/rc4: src not fully XORed")
}
runtime.KeepAlive(c)
}
167 changes: 167 additions & 0 deletions rc4_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package openssl_test

import (
"bytes"
"fmt"
"testing"

"github.com/golang-fips/openssl/v2"
)

type rc4Test struct {
key, keystream []byte
}

var golden = []rc4Test{
// Test vectors from the original cypherpunk posting of ARC4:
// https://groups.google.com/group/sci.crypt/msg/10a300c9d21afca0?pli=1
{
[]byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef},
[]byte{0x74, 0x94, 0xc2, 0xe7, 0x10, 0x4b, 0x08, 0x79},
},
{
[]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
[]byte{0xde, 0x18, 0x89, 0x41, 0xa3, 0x37, 0x5d, 0x3a},
},
{
[]byte{0xef, 0x01, 0x23, 0x45},
[]byte{0xd6, 0xa1, 0x41, 0xa7, 0xec, 0x3c, 0x38, 0xdf, 0xbd, 0x61},
},

// Test vectors from the Wikipedia page: https://en.wikipedia.org/wiki/RC4
{
[]byte{0x4b, 0x65, 0x79},
[]byte{0xeb, 0x9f, 0x77, 0x81, 0xb7, 0x34, 0xca, 0x72, 0xa7, 0x19},
},
{
[]byte{0x57, 0x69, 0x6b, 0x69},
[]byte{0x60, 0x44, 0xdb, 0x6d, 0x41, 0xb7},
},
{
[]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
[]byte{
0xde, 0x18, 0x89, 0x41, 0xa3, 0x37, 0x5d, 0x3a,
0x8a, 0x06, 0x1e, 0x67, 0x57, 0x6e, 0x92, 0x6d,
0xc7, 0x1a, 0x7f, 0xa3, 0xf0, 0xcc, 0xeb, 0x97,
0x45, 0x2b, 0x4d, 0x32, 0x27, 0x96, 0x5f, 0x9e,
0xa8, 0xcc, 0x75, 0x07, 0x6d, 0x9f, 0xb9, 0xc5,
0x41, 0x7a, 0xa5, 0xcb, 0x30, 0xfc, 0x22, 0x19,
0x8b, 0x34, 0x98, 0x2d, 0xbb, 0x62, 0x9e, 0xc0,
0x4b, 0x4f, 0x8b, 0x05, 0xa0, 0x71, 0x08, 0x50,
0x92, 0xa0, 0xc3, 0x58, 0x4a, 0x48, 0xe4, 0xa3,
0x0a, 0x39, 0x7b, 0x8a, 0xcd, 0x1d, 0x00, 0x9e,
0xc8, 0x7d, 0x68, 0x11, 0xf2, 0x2c, 0xf4, 0x9c,
0xa3, 0xe5, 0x93, 0x54, 0xb9, 0x45, 0x15, 0x35,
0xa2, 0x18, 0x7a, 0x86, 0x42, 0x6c, 0xca, 0x7d,
0x5e, 0x82, 0x3e, 0xba, 0x00, 0x44, 0x12, 0x67,
0x12, 0x57, 0xb8, 0xd8, 0x60, 0xae, 0x4c, 0xbd,
0x4c, 0x49, 0x06, 0xbb, 0xc5, 0x35, 0xef, 0xe1,
0x58, 0x7f, 0x08, 0xdb, 0x33, 0x95, 0x5c, 0xdb,
0xcb, 0xad, 0x9b, 0x10, 0xf5, 0x3f, 0xc4, 0xe5,
0x2c, 0x59, 0x15, 0x65, 0x51, 0x84, 0x87, 0xfe,
0x08, 0x4d, 0x0e, 0x3f, 0x03, 0xde, 0xbc, 0xc9,
0xda, 0x1c, 0xe9, 0x0d, 0x08, 0x5c, 0x2d, 0x8a,
0x19, 0xd8, 0x37, 0x30, 0x86, 0x16, 0x36, 0x92,
0x14, 0x2b, 0xd8, 0xfc, 0x5d, 0x7a, 0x73, 0x49,
0x6a, 0x8e, 0x59, 0xee, 0x7e, 0xcf, 0x6b, 0x94,
0x06, 0x63, 0xf4, 0xa6, 0xbe, 0xe6, 0x5b, 0xd2,
0xc8, 0x5c, 0x46, 0x98, 0x6c, 0x1b, 0xef, 0x34,
0x90, 0xd3, 0x7b, 0x38, 0xda, 0x85, 0xd3, 0x2e,
0x97, 0x39, 0xcb, 0x23, 0x4a, 0x2b, 0xe7, 0x40,
},
},
}

func TestRC4Golden(t *testing.T) {
if !openssl.SupportsRC4() {
t.Skip("RC4 is not supported")
}
for gi, g := range golden {
data := make([]byte, len(g.keystream))
for i := range data {
data[i] = byte(i)
}

expect := make([]byte, len(g.keystream))
for i := range expect {
expect[i] = byte(i) ^ g.keystream[i]
}

for size := 1; size <= len(g.keystream); size++ {
c, err := openssl.NewRC4Cipher(g.key)
if err != nil {
t.Fatalf("#%d: NewCipher: %v", gi, err)
}

off := 0
for off < len(g.keystream) {
n := len(g.keystream) - off
if n > size {
n = size
}
desc := fmt.Sprintf("#%d@[%d:%d]", gi, off, off+n)
src := data[off : off+n]
expect := expect[off : off+n]
dst := make([]byte, len(src))
c.XORKeyStream(dst, src)
for i, v := range dst {
if v != expect[i] {
t.Fatalf("%s: mismatch at byte %d:\nhave %x\nwant %x", desc, i, dst, expect)
}
}
off += n
}
}
}
}

func TestRC4Block(t *testing.T) {
if !openssl.SupportsRC4() {
t.Skip("RC4 is not supported")
}
c1a, _ := openssl.NewRC4Cipher(golden[0].key)
c1b, _ := openssl.NewRC4Cipher(golden[1].key)
data1 := make([]byte, 1<<20)
for i := range data1 {
c1a.XORKeyStream(data1[i:i+1], data1[i:i+1])
c1b.XORKeyStream(data1[i:i+1], data1[i:i+1])
}

c2a, _ := openssl.NewRC4Cipher(golden[0].key)
c2b, _ := openssl.NewRC4Cipher(golden[1].key)
data2 := make([]byte, 1<<20)
c2a.XORKeyStream(data2, data2)
c2b.XORKeyStream(data2, data2)

if !bytes.Equal(data1, data2) {
t.Fatalf("bad block")
}
}

func benchmarkRC4(b *testing.B, size int64) {
if !openssl.SupportsRC4() {
b.Skip("RC4 is not supported")
}
buf := make([]byte, size)
c, err := openssl.NewRC4Cipher(golden[0].key)
if err != nil {
panic(err)
}
b.SetBytes(size)

for i := 0; i < b.N; i++ {
c.XORKeyStream(buf, buf)
}
}

func BenchmarkRC4_128(b *testing.B) {
benchmarkRC4(b, 128)
}

func BenchmarkRC4_1K(b *testing.B) {
benchmarkRC4(b, 1024)
}

func BenchmarkRC4_8K(b *testing.B) {
benchmarkRC4(b, 8096)
}
2 changes: 2 additions & 0 deletions shims.h
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,9 @@ DEFINEFUNC(const GO_EVP_CIPHER_PTR, EVP_des_ecb, (void), ()) \
DEFINEFUNC(const GO_EVP_CIPHER_PTR, EVP_des_cbc, (void), ()) \
DEFINEFUNC(const GO_EVP_CIPHER_PTR, EVP_des_ede3_ecb, (void), ()) \
DEFINEFUNC(const GO_EVP_CIPHER_PTR, EVP_des_ede3_cbc, (void), ()) \
DEFINEFUNC(const GO_EVP_CIPHER_PTR, EVP_rc4, (void), ()) \
DEFINEFUNC_RENAMED_3_0(int, EVP_CIPHER_get_block_size, EVP_CIPHER_block_size, (const GO_EVP_CIPHER_PTR cipher), (cipher)) \
DEFINEFUNC(int, EVP_CIPHER_CTX_set_key_length, (GO_EVP_CIPHER_CTX_PTR x, int keylen), (x, keylen)) \
DEFINEFUNC(void, EVP_CIPHER_CTX_free, (GO_EVP_CIPHER_CTX_PTR arg0), (arg0)) \
DEFINEFUNC(int, EVP_CIPHER_CTX_ctrl, (GO_EVP_CIPHER_CTX_PTR ctx, int type, int arg, void *ptr), (ctx, type, arg, ptr)) \
DEFINEFUNC(GO_EVP_PKEY_PTR, EVP_PKEY_new, (void), ()) \
Expand Down