mirror of https://gogs.blitter.com/RLabs/xs
211 lines
4.9 KiB
Go
211 lines
4.9 KiB
Go
// WANDERER - a crypto doodle that appears to give adequate
|
|
// protection to data in a stream cipher context
|
|
//
|
|
// Properties visualized using https://github.com/circulosmeos/circle
|
|
package wanderer
|
|
|
|
// TODOs:
|
|
// -define s-box rotation/shuffle schema
|
|
// -devise p-box schema (? Meh. Need to blockify & re-streamify to do this)
|
|
// ...
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"time"
|
|
|
|
mtwist "blitter.com/go/mtwist"
|
|
)
|
|
|
|
const (
|
|
keylen = 512
|
|
sboxCount = keylen / 8
|
|
)
|
|
|
|
// Given input byte x (treated as 2-bit dirs),
|
|
// 'walk' box applying XOR of each position (E/S/W/N) given box
|
|
// dimensions w,h
|
|
// NOTE to ensure reachability of all values within a box, w,h
|
|
// should not each exceed 3 and should probably stay at 2, to
|
|
// give more even coverage given random input.
|
|
func walkingXOR(key, s []byte, w, h int, x byte) (r byte) {
|
|
i := 0
|
|
r = x
|
|
for sidx := range key {
|
|
ktemp := key[sidx]
|
|
r = r ^ (s[i])
|
|
for shift := uint(0); shift < 8; shift += 2 {
|
|
// fmt.Println("i:", i, "r:", r)
|
|
dir := (ktemp >> shift) & 0x03
|
|
switch dir {
|
|
case 0:
|
|
i = i + 1
|
|
i = i % len(s)
|
|
case 1:
|
|
i = i + w
|
|
i = i % len(s)
|
|
case 2:
|
|
if i%w != 0 {
|
|
i = i - 1
|
|
} else {
|
|
i = i + w - 1
|
|
}
|
|
case 3:
|
|
if i >= w {
|
|
i = i - w
|
|
} else {
|
|
i = len(s) + i - w
|
|
}
|
|
}
|
|
r = r ^ (s[i])
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (c *Cipher) genSBoxen(n uint) {
|
|
c.sboxen = make([][]byte, n)
|
|
var idx uint
|
|
for ; idx < n; idx++ {
|
|
c.sboxen[idx] = make([]byte, c.sw*c.sh)
|
|
_, _ = c.prng.Read(c.sboxen[idx])
|
|
}
|
|
//fmt.Fprintf(os.Stderr, "sboxen[0]:%v\n", c.sboxen[0])
|
|
}
|
|
|
|
// Mutate the session key (intended to be called as encryption
|
|
// proceeds), so that the 'walk path' through sboxes also does so.
|
|
func (c *Cipher) keyUpdate(perturb byte) {
|
|
c.k[c.kidx] = c.k[c.kidx] ^ c.k[(c.kidx+1)%uint(len(c.k))]
|
|
c.k[c.kidx] = c.k[c.kidx] ^ byte((c.prng.Int63()>>4)%256)
|
|
c.kidx = (c.kidx + uint(perturb)) % uint(len(c.k))
|
|
}
|
|
|
|
// slow - perturb a single octet of a single sbox for each octet
|
|
// (CV = ~8.725% over 700 MiB of 0-byte pt)
|
|
func (c *Cipher) sboxUpdateA(perturb byte) {
|
|
c.sboxen[perturb%sboxCount][int(perturb)%(c.sw+c.sh)] ^=
|
|
perturb
|
|
}
|
|
|
|
// slower - perturb a single sbox for each octet
|
|
// (CV = ~?% over 700 MiB of 0-byte pt)
|
|
func (c *Cipher) sboxUpdateB(perturb byte) {
|
|
lim := c.sw * c.sh
|
|
for idx := 0; idx < lim; idx++ {
|
|
c.sboxen[perturb%sboxCount][idx] ^= perturb
|
|
}
|
|
}
|
|
|
|
// slowest -- full sbox re-gen after each octet
|
|
// (but lowest CV, ~0.05% over 700MiB of 0-byte pt)
|
|
func (c *Cipher) sboxUpdateC(perturb byte) {
|
|
c.genSBoxen(sboxCount)
|
|
//c.sboxen[perturb%sboxCount][int(perturb)%(c.sw+c.sh)] ^=
|
|
// perturb
|
|
}
|
|
|
|
type Cipher struct {
|
|
prng *mtwist.MT19937_64
|
|
r io.Reader
|
|
w io.Writer
|
|
k []byte
|
|
kidx uint
|
|
sboxen [][]byte
|
|
sw int
|
|
sh int
|
|
sctr int // TODO: used to count down to re-keying & sbox regen
|
|
mode int
|
|
n byte
|
|
}
|
|
|
|
func NewCodec(r io.Reader, w io.Writer, mode int, key []byte, width, height int) (c *Cipher) {
|
|
c = &Cipher{}
|
|
c.prng = mtwist.New()
|
|
if len(key) == 0 {
|
|
c.k = []byte(fmt.Sprintf("%s", time.Now()))
|
|
} else {
|
|
c.k = key
|
|
}
|
|
c.prng.SeedFullState(c.k)
|
|
|
|
// Discard first 64 bytes of MT output
|
|
for idx := 0; idx < 64; idx++ {
|
|
_ = c.prng.Int63()
|
|
}
|
|
c.mode = mode
|
|
c.r = r
|
|
c.w = w
|
|
c.sw = width
|
|
c.sh = height
|
|
c.sctr = c.sw // sbox ctr: countdown to regen sboxes
|
|
c.n = 0
|
|
c.genSBoxen(sboxCount)
|
|
|
|
// fmt.Printf("%+v\n", sboxen)
|
|
// c.buf = make([]byte, 4)
|
|
return c
|
|
}
|
|
|
|
func (c *Cipher) Read(p []byte) (n int, err error) {
|
|
n, err = c.r.Read(p)
|
|
if err == nil {
|
|
for idx := 0; idx < n; idx++ {
|
|
p[idx] = c.yield(p[idx])
|
|
}
|
|
}
|
|
return n, err
|
|
}
|
|
|
|
func (c *Cipher) Write(p []byte) (n int, err error) {
|
|
n, err = c.w.Write(p)
|
|
return n, err
|
|
}
|
|
|
|
func (c *Cipher) yield(pt byte) (ct byte) {
|
|
ct = walkingXOR(c.k, c.sboxen[c.n], c.sw, c.sh, pt)
|
|
c.n = (c.n + 1) % byte(len(c.sboxen))
|
|
c.keyUpdate(ct ^ pt) // must be equal in either encrypt/decrypt dirs
|
|
switch c.mode {
|
|
case 0:
|
|
// [nothing - varA]
|
|
break
|
|
case 1:
|
|
c.sboxUpdateA(ct ^ pt) // varA
|
|
case 2:
|
|
c.sboxUpdateB(ct ^ pt) // varB
|
|
case 3:
|
|
c.sboxUpdateC(ct ^ pt) // varC
|
|
default:
|
|
// [nothing]
|
|
}
|
|
// c.sctr = c.sctr - 1
|
|
// if c.sctr == 0 {
|
|
// c.genSBoxen(sboxCount)
|
|
// c.sctr = c.sw
|
|
// }
|
|
return ct
|
|
}
|
|
|
|
// XORKeyStream XORs each byte in the given slice with a byte from the
|
|
// cipher's key stream. Dst and src must overlap entirely or not at all.
|
|
//
|
|
// If len(dst) < len(src), XORKeyStream should panic. It is acceptable
|
|
// to pass a dst bigger than src, and in that case, XORKeyStream will
|
|
// only update dst[:len(src)] and will not touch the rest of dst.
|
|
//
|
|
// Multiple calls to XORKeyStream behave as if the concatenation of
|
|
// the src buffers was passed in a single run. That is, Stream
|
|
// maintains state and does not reset at each XORKeyStream call.
|
|
func (c *Cipher) XORKeyStream(dst, src []byte) {
|
|
//fmt.Printf("len dst:%d len src:%d\n", len(dst), len(src))
|
|
if len(dst) < len(src) {
|
|
panic(errors.New("len(dst) < len(src)"))
|
|
}
|
|
|
|
for idx, v := range src {
|
|
dst[idx] = c.yield(v)
|
|
}
|
|
}
|