ARC4 vector creation

This page documents the code that was used to generate the ARC4 test vectors for key lengths not available in RFC 6229. All the vectors were generated using OpenSSL and verified with Go.

Creation

cryptography was modified to support ARC4 key lengths not listed in RFC 6229. Then the following Python script was run to generate the vector files.

# This file is dual licensed under the terms of the Apache License, Version
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
# for complete details.

from __future__ import absolute_import, division, print_function

import binascii

from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import ciphers
from cryptography.hazmat.primitives.ciphers import algorithms


_RFC6229_KEY_MATERIALS = [
    (
        True,
        8 * "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20",
    ),
    (
        False,
        8 * "1ada31d5cf688221c109163908ebe51debb46227c6cc8b37641910833222772a",
    ),
]


_RFC6229_OFFSETS = [
    0,
    16,
    240,
    256,
    496,
    512,
    752,
    768,
    1008,
    1024,
    1520,
    1536,
    2032,
    2048,
    3056,
    3072,
    4080,
    4096,
]


_SIZES_TO_GENERATE = [160]


def _key_for_size(size, keyinfo):
    msb, key = keyinfo
    if msb:
        return key[: size // 4]
    else:
        return key[-size // 4 :]


def _build_vectors():
    count = 0
    output = []
    key = None
    plaintext = binascii.unhexlify(32 * "0")
    for size in _SIZES_TO_GENERATE:
        for keyinfo in _RFC6229_KEY_MATERIALS:
            key = _key_for_size(size, keyinfo)
            cipher = ciphers.Cipher(
                algorithms.ARC4(binascii.unhexlify(key)),
                None,
                default_backend(),
            )
            encryptor = cipher.encryptor()
            current_offset = 0
            for offset in _RFC6229_OFFSETS:
                if offset % 16 != 0:
                    raise ValueError(
                        "Offset {} is not evenly divisible by 16".format(
                            offset
                        )
                    )
                while current_offset < offset:
                    encryptor.update(plaintext)
                    current_offset += len(plaintext)
                output.append("\nCOUNT = {}".format(count))
                count += 1
                output.append("KEY = {}".format(key))
                output.append("OFFSET = {}".format(offset))
                output.append(
                    "PLAINTEXT = {}".format(binascii.hexlify(plaintext))
                )
                output.append(
                    "CIPHERTEXT = {}".format(
                        binascii.hexlify(encryptor.update(plaintext))
                    )
                )
                current_offset += len(plaintext)
            assert not encryptor.finalize()
    return "\n".join(output)


def _write_file(data, filename):
    with open(filename, "w") as f:
        f.write(data)


if __name__ == "__main__":
    _write_file(_build_vectors(), "arc4.txt")

Download link: generate_arc4.py

Verification

The following Go code was used to verify the vectors.

package main

import (
	"bufio"
	"bytes"
	"crypto/rc4"
	"encoding/hex"
	"fmt"
	"os"
	"strconv"
	"strings"
)

func unhexlify(s string) []byte {
	bytes, err := hex.DecodeString(s)
	if err != nil {
		panic(err)
	}
	return bytes
}

type vectorArgs struct {
	count      string
	offset     uint64
	key        string
	plaintext  string
	ciphertext string
}

type vectorVerifier interface {
	validate(count string, offset uint64, key, plaintext, expectedCiphertext []byte)
}

type arc4Verifier struct{}

func (o arc4Verifier) validate(count string, offset uint64, key, plaintext, expectedCiphertext []byte) {
	if offset%16 != 0 || len(plaintext) != 16 || len(expectedCiphertext) != 16 {
		panic(fmt.Errorf("Unexpected input value encountered: offset=%v; len(plaintext)=%v; len(expectedCiphertext)=%v",
			offset,
			len(plaintext),
			len(expectedCiphertext)))
	}
	stream, err := rc4.NewCipher(key)
	if err != nil {
		panic(err)
	}

	var currentOffset uint64 = 0
	ciphertext := make([]byte, len(plaintext))
	for currentOffset <= offset {
		stream.XORKeyStream(ciphertext, plaintext)
		currentOffset += uint64(len(plaintext))
	}
	if !bytes.Equal(ciphertext, expectedCiphertext) {
		panic(fmt.Errorf("vector mismatch @ COUNT = %s:\n  %s != %s\n",
			count,
			hex.EncodeToString(expectedCiphertext),
			hex.EncodeToString(ciphertext)))
	}
}

func validateVectors(verifier vectorVerifier, filename string) {
	vectors, err := os.Open(filename)
	if err != nil {
		panic(err)
	}
	defer vectors.Close()

	var segments []string
	var vector *vectorArgs

	scanner := bufio.NewScanner(vectors)
	for scanner.Scan() {
		segments = strings.Split(scanner.Text(), " = ")

		switch {
		case strings.ToUpper(segments[0]) == "COUNT":
			if vector != nil {
				verifier.validate(vector.count,
					vector.offset,
					unhexlify(vector.key),
					unhexlify(vector.plaintext),
					unhexlify(vector.ciphertext))
			}
			vector = &vectorArgs{count: segments[1]}
		case strings.ToUpper(segments[0]) == "OFFSET":
			vector.offset, err = strconv.ParseUint(segments[1], 10, 64)
			if err != nil {
				panic(err)
			}
		case strings.ToUpper(segments[0]) == "KEY":
			vector.key = segments[1]
		case strings.ToUpper(segments[0]) == "PLAINTEXT":
			vector.plaintext = segments[1]
		case strings.ToUpper(segments[0]) == "CIPHERTEXT":
			vector.ciphertext = segments[1]
		}
	}
	if vector != nil {
		verifier.validate(vector.count,
			vector.offset,
			unhexlify(vector.key),
			unhexlify(vector.plaintext),
			unhexlify(vector.ciphertext))
	}
}

func main() {
	validateVectors(arc4Verifier{}, "vectors/cryptography_vectors/ciphers/ARC4/arc4.txt")
	fmt.Println("ARC4 OK.")
}

Download link: verify_arc4.go