Skip to content

AdamColton/cyclicKey

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

12 Commits
 
 
 
 
 
 
 
 

Repository files navigation

Cyclic Key Cryptography

Let's start with: THIS WAS AN EXPERIMENT, it is broken. Thanks to /r/crypto. There is a detailed explanation of the attack at the bottom.

The rest of this document, except the section on the attack, is unaltered from it's original state. I've left it as I think the logic is interesting and perhaps someone could build on it. But this is very broken and not at all secure.

Motivation

My specific motivation regards Onion Routing, but this cipher could have other uses. I wanted to assign a one time use key to every node in an onion-path (embedded within the onion package) so that a message could be mutated at each step and return to it's original state when all keys had been applied (with the intended recipient holding the last key). It was important that no two keys could be proven to belong to the same set (unless they were identical, or the entire set is known). And that no two message states could be shown to be instance of the message. More specifically, because every node sees two states (incoming and out going) the combination of a message-state and key cannot be shown to correspond to any other message-state except the one generated by applying the key.

The Math

I cannot really call this original, it's a slight variation on the Diffie-Hellman key exchange, though I originally came to the idea through the Pohlig-Hellman Algorithm via this post.

The algorithm is based on the discrete log problem and uses much of the same math as other ciphers relying on that mechanism. In the following example we will create a keyset with 3 keys, ^-1 refers to modular inversion.

-- Start with
p : a prime
r : a primitive root of p
m : a message in (1, p-1)
-- Choose
x,y : keys in (1, p-1)
i : inversion direction, a bool
-- Compute
c : the compound key
c = (x+y) % (p-1)
-- Cipher
c1 = (m * (r^x)) % p
c1 = i ?: c1 ^ -1

c2 = (c1 * (r^y)) % p
c2 = i ?: c2 ^ -1

c3 = (c2 * (r^y)) % p
c3 = !i ?: c3 ^ -1
-- Result
c3 == m

The inversion flag, i, would be distributed with (or encoded so it can be extracted from) the key. Because this is randomly chosen, it is not an indication of which key is the compound key. The compound key is indistinguishable.

This works because

((x^a) % y) * ((x^b) % y) == (x ^ (a+b)) % y)
for any values of a,b,x and y

And
(r ^ (a+b)) % p == (r ^ ((a+b)%(p-1))) % p
for any values of a and b, and any p where r is a primitive
It may also work for any values, but those are the only ones I've confirmed so far

Implementation

I chose a fixed prime of 257 (2^8 + 1). This has many advantageous properties for the algorithm. The key and message must be broken up into segments that lie between 1 and p-1. Using 257 allows to take the bytes of a message (cast to a larger type) and add 1. This ensures we are always in the necessary range. Another advantage is that the algorithm for finding cyclic routes involves finding the greatest common denominator between a number (the root index) and p-1. Because p-1 = 2^8, we can bypass this check by starting the root index at 1 and incrementing by 2. The final advantage is that all necessary computations are small enough to pre-compute and cache. This provides an enormous gain in speed. The cache consumes 32,896 B.

I will not provide a detailed explanation here, but because of the way I wish to apply this to onion-routing, the cryptographic requirements are lower than usual, but a premium is placed on overhead and speed. For this reason, I've chosen a key of 2^80 and may even reduce that to 2^64. This is why the KeyLength defaults to 10 bytes. This variable is intentionally exposed, the algorithm works with any number of bytes as the key.

Full implementation pseudo code; I will use some list comprehensions in the notation. I am not notating type conversions, so in the cases that an operation could overflow a type, assume that we've made the proper conversions (looking at the source code will show this). This also does not show the key rotation which will be described after.

-- Given
p : 257
s : p-1
m : byte array
r : byte array of primitive roots
-- Generate Keys
n : number of keys
l : length of each key
i : RAND_BOOL // inversion
k = [ [ RAND_BYTE foreach l] foreach n] // two dimensional byte array size n x l
ki = [i foreach n]
k[l] = [ sum([k[x][y] for x in n]) % (p-1) for y in l ]
ki[l] = !i
-- Cipher Algorithm
Arguments(
  m: message // 1d byte array
  k: key // 1d byte array (we're doing this step for one key)
  i: inversion //bool
)
c : byte array with len(c) = len(m)

for x in len(m):
  kp = 1 // key product
  for y in len(k):
    kp *= (r[x+y] ^ k[y]) % p
  kp = i ?: kp^-1
  c[x] = (m*kp) % p

The final details is the key rotation. In the above code, if x+y > 255, kp will repeat. This will easily break the security we're after. This only allows us to encipher 127 bytes. The keys need to be rotated in such a way that the output will not repeat but the relationship between the original keys is maintained. The solution is to multiply the key segments, but for each cipher operations, the same rotation values must be used. Providing some sort of seed value would provide an identity, which is exactly what this scheme is trying to prevent, so a constant set of rotations is used.

The key rotation can be a weakness. If too many rotation values overlap, the cipher becomes weak against chosen-plain-text-attack (and possibly others). I have tried a few different rotation schemes to mitigate this. The best option I have come up with so far has been to use xorShift to generate the rotations. XorShift is not a cryptographically strong pseudo-random generator, but it does not need to be. It was chosen because it is fast and light to implement. The current seeds do not have more than 4 overlapping values in the first 278K sets of 10. This is suitable for about 32M of data. I am currently searching for better seeds.

Performance

For a fixed key length, the algorithm runs in O(n) time, n being message length.

Running the benchmarks on an Ubuntu server, the cyclic key cipher performed 4x slower than AES. I consider this to be more than acceptable. The AES implementation in Go has been highly optimized. This version has gone through several rounds of optimization, the most effective have been moving some of the computation to tables. With Go's bounds checking, this will incur notable overhead. I would like to try moving this into cgo to see what the performance gain is. However, even at this speed, the cyclic key cipher performs an operation that cannot be done with AES (at least not to my knowledge) and the performance loss is there for acceptable if that operation is necessary.

Table Optimizations

Modulus Inversion Table

The modulus inversion table (invTbl) is straightforward. We find the modular inversion of every number from 1 to 256 with respect to the prime 257. Because this skips 0, to efficiently fill the array, every value is decremented by 1. Because inversions come in pairs, we populate both values.

Power Mod Table

The power mode table (pmTbl) is more complex. The powerMod function takes 2 arguments, ri (root index) and e (exponent) and returns (r**e) % p, where r is the root at index i. For the algorithm, ri will always be in (0,127). The exponent (e) can be any number from 0 to 256. This gives us pmTbl[ri*257 + e].

The Attack

Because the prime is so small, 257, we can easily build a discrete log table. This turns

r1^k1 * r2^k2 * ... * rn^kn = kp

into

r1^k1 * r2^k2 * ... * rn^kn = kp
k1*dlog(r1) + k2*dlog(r2) + ... + kn*dlog(rn) = dlog(kp)

Two details that had me confused (and thanks again to /r/crypto for their help), first dlog requires a base.

( b^x = y       ) % 257
( dlog_b(y) = x ) % 257

But that's not terribly important, just as in "normal" algebra, we just use ln most of the time, even when we don't care that the base is e. The base does need to be a primitive root, or there will be holes in the table.

The second detail is that the dlog is computed in GF(N), in our case N is 257. But when we apply dlog, the equation moves into the toitent of N, in our case 256. So the example above is more accurately stated as

( r1^k1 * r2^k2 * ... * rn^kn = kp                         ) % 257
( k1*dlog(r1) + k2*dlog(r2) + ... + kn*dlog(rn) = dlog(kp) ) % 256

From here the key bytes can be recovered as a set of linear equations. In practice, it gets a bit tricky because the modular inversions don't always line up. There is a demonstration of the attack in the tests.

About

A test implementation of a symmetric cyclic key algorithm

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages