A finite field requires a prime $p$. I'll assume you want $p=2$. Then you also need a power $d$ in order to do the addition. Numbers in the field are just lists of numbers -- the list has length $d$ and each entry in the list is one of the numbers $0$, $1$, ..., all the way up to $p-1$. I'll assume you want $d=4$. This means the elements of the field are:
- $(0,0,0,0)$,
- $(0,0,0,1)$,
- $(0,0,1,0)$,
- ...
- $(1,1,1,1)$
When $p=2$ more or less everyone agrees it is a good idea to think of these lists (of bits) as binary numbers and at this point it doesn't matter much which order you think of the bits. I wrote them as MSB-first, so:
- $(0,0,0,0)$
= 0b0000 = 0 = 0x0
- $(0,0,0,1)$
= 0b0001 = 1 = 0x1
- $(0,0,1,0)$
= 0b0010 = 2 = 0x2
- ...
- $(1,1,1,1)$
= 0b1111 = 15 = 0xf
but when you add, you are just adding the numbers in the list, not the real binary number. Luckily computers are very quick at this (quicker than addition on smaller computers where there is a difference) because this is called XOR and it can ignore the carry, just like you suspected. Also if you don't like MSB-first, then luckily addition doesn't care what order you want the bits as long as you are consistent with yourself.
Multiplication is a little more complicated and larger computers now have instructions meant to speed this up. I'll use an old technique that can help you understand some related ideas in computer science.
But now we need to know more than just the size $d$ of the lists. We need to choose a specific field element to be a sort of "carry" for multiplying.
I'll choose 0x3
as my carry (you could use 0x9
or 0xf
too). No matter the $d$, it helps to understand multiplying by 0x2
first. Much like regular numbers, multiplying by two can use the faster instruction n << 1
that shifts the bits to the left. However, in a field you need to be able to divide too, and if you shift to the left too much all the bits disappear! So like with addition finite fields change the rules ever so slightly, but this time they INCLUDE a carry while normally n << 1
does not. If the highest bit is set, n & 0x8
, then shifting it left normally would lose it (take it outside the field), so instead of leaving that leftmost bit (which would be 0x10
now, too big for the field), we replace it with the carry. You can XOR by 0x13
if you keep track of that outside bit (0x10 ^ 0x13
is just 0x03
), or just XOR by 0x03
if you ignore the higher bits (easier to do when d=8, d=16, d=32, or d=64).
So "multiply by 0x2
" is (n<<1)^((n&0x8)?0x13:0x00)
. Multiply by 0x1 is easy: just leave it alone (just like a regular number 1*7 = 7
, the 1 doesn't change anything).
To multiply by a general number we multiply by each of the bits. So to multiply by 0x6
we write 0x6 = 0x4 ^ 0x2
and then distribute to get "multiply by 0x4 and xor it with the answer you get when xor'ing by 0x2".
int multiply(int a,int b) {
int ret = 0; // start with nothing to return, we'll add in as we go
if (b&1) ret ^= a; // multiply by 1 means "keep n"
b >>= 1; // done with least bit, scoot the multiplier bits over to the right
a = (a<<1)^((a&0x8)?0x13:0x00); // multiply by 2, including the carry
if (b&1) ret ^= a; // since we've changed b, this is really the multiply by 2, but we've already changed a, so it uses the same code as multiply by 1, so smart
b >>= 1;
a = (a<<1)^((a&0x8)?0x13:0x00);
if (b&1) ret ^= a; // this handles b & 0x4
b >>= 1;
a = (a<<1)^((a&0x8)?0x13:0x00);
if (b&1) ret ^= a; // this handles b & 0x8
assert(b<2); // no more bits for you!
return ret;
}
To use a different carry for multiply by 0x2
you just change the 0x13
to 0x19
or 0x1f
. As @Gae. S. mentions, these are the irreducible polynomials of degree 4 over GF(2).
Bit order matters for multiplication, but luckily whether people use MSB or LSB, they usually use the same code above (if they swap what order they write down binary numbers, they also swap what order they write down polynomials, and so in the code it becomes the same except for printing).