MCS 320 Project One due Monday 26 September 2005 at 2PM
| > | restart; |
This worksheet contains a possible solution to the project.
Assignment One: make your own worksheet model
Using the given message
| > | message := "mcs 320 is fun": |
we will walk through the three steps in the process:
1. encode/decode the message, converting a string into a sequence of bits;
2. encrypt the sequence of bits as a subset sum problem;
3. decrypt the sequence using the private key.
1. Encode/decode a string of characters as a sequence of bits
We use the following function which takes on input a string and returns a sequence of bytes, each byte is a sequence of at most 8 bits:
| > | encode := s -> map(x->convert(x,base,2),convert(s,bytes)): |
| > | encoded_message := encode(message); |
![]()
![]()
Our message is now encoded as a list of bit sequences.
The opposite of encode is decode:
| > | evalbase2 := x -> sum(x[i]*2^(i-1),i=1..nops(x)): |
| > | decode := s -> convert(map(x->evalbase2(x),s),bytes): |
| > | decode(encoded_message); |
2. Pack the bytes as a subset sum problem
The knapsack cryptosystem is a public key cryptosystem. The public key is a sequence of weights, generated from a sequence of super increasing weights. A sequence of weights is super increasing if every element in the sequence is larger than the sum of all previous elements in the sequence. For example:
| > | w := [2,3,6,13,27,52,105,220]: # a super increasing sequence of weights |
But this super increasing sequence of weights is secret. The public key is generated from two relatively prime numbers u and v, which must be higher than the sum of the weights. If we pick two prime numbers, then gcd(u,v) = 1. This requirement is needed to be able to divide by v, modulo u.
| > | bound := add(w[i],i=1..nops(w)): |
| > | u := nextprime(bound): v := nextprime(234): |
| > | public_key := map(t->v*t mod u,w); |
To encrypt the list of bit sequences, we multiply the bits with the weights, as we would pack a knapsack. We pick the elements of weights w[i] for which the corresponding bit equals 1. Every byte is now represented by the sum of the weights selected in the knapsack. Our message is thus encrypted as a list of sums. We use the following functions:
| > | pack_byte := (w,x) -> sum(w['i']*x['i'],'i'=1..min(nops(w),nops(x))): |
| > | pack_bytes := (w,x) -> map(t->pack_byte(w,t),x): |
| > | encrypt := (key,s) -> pack_bytes(key,encode(s)): |
| > | encrypted_message := encrypt(public_key,message); |
The message is encrypted with the public key of the intended recipient.
3. Decrypting is easy for super increasing weights
The intended recipient of the message knows u,v, and w. Since w is super increasing, solving the subset sum problem is as easy as computing the binary decomposition of a number.
| > | unpack_byte := proc(w,s)
description `solves the subset sum problem for super increasing w`: local n,i,x,r: x := []: n := nops(w): r := s: for i from n by -1 to 1 do if r >= w[i] then x := [1,op(x)]: r := r-w[i]: elif nops(x) > 0 then x := [0,op(x)]: end if: end do: return x: end proc: |
| > | unpack_bytes := (w,x) -> map(t->unpack_byte(w,t),x): |
| > | private_key := 1/v mod u: |
| > | decrypt := (key,w,s) -> decode(unpack_bytes(w,map(t->key*t mod u,s))): |
| > | decrypt(private_key,w,encrypted_message); |
Assignment Two: build your own cryptosystem
The cryptosystem consists of a super increasing sequence of weights, which is turned into a general sequence using u and v.
| > | my_w := [1,2,4,8,16,32,64,128]: |
| > | my_bound := add(w[i],i=1..nops(w)): |
| > | my_u := nextprime(my_bound); my_v := nextprime(313); |
| > | my_public_key := map(t->my_v*t mod my_u,my_w); |
| > | my_private_key := 1/my_v mod my_u; |
| > | my_message := "Symbolic computation is about computing with symbols.\n
Like numerical computations may suffer from roundoff and ill conditioning,\n we must be careful about false simplifications and expression swell."; |
| > | my_encrypted_message := encrypt(my_public_key,my_message); |
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
| > | decrypt(my_private_key,my_w,my_encrypted_message); |
Assignment Three: small numbers are insecure
The purpose of this assignement is to show that with the LLL algorithm we can decrypt a large portion of the message, if small numbers are chosen. For larger numbers, this is no longer the case.
| > | with(IntegerRelations,LLL): |
We will use this auxiliary function, which returns true if r is a bit sequence, false otherwise.
| > | is_byte := proc(r)
local i: for i from 1 to nops(r) do if r[i] < 0 or r[i] > 1 then return false; end if; end do; return true; end proc: |
The use of the LLL to decrypt one character is done by the following procedure:
| > | crack := proc(public_key,s)
local id,v,A,B,r,c,i: id := matrix(9,8,(i,j) -> piecewise(i=j,1,0)): v := matrix(9,1,[-op(public_key),s]): A := linalg[augment](id,v): B := [seq(convert(linalg[row](A,i),list),i=1..9)]: r := LLL(B,'integer'); for i from 1 to nops(r) do if is_byte(r[i]) then return decode([r[i]]): end if; end do: return "": end proc: |
and applied to a encrypted message as follows:
| > | map(t->crack(my_public_key,t),my_encrypted_message); |
![]()
![]()
![]()
![]()
![]()
![]()
We see that except for the "y", the LLL algorithm is very successful in decrypting the message.
We now choose numbers as large as 12 digits to generate our public key:
| > | randomize(1): |
| > | new_u := nextprime(rand()); new_v := nextprime(rand()); new_public_key := map(t->new_v*t mod new_u,my_w); new_private_key := 1/new_v mod new_u; |
And encrypt our message with these larger numbers:
| > | new_encrypted_message := encrypt(new_public_key,my_message); |
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
| > | map(t->crack(new_public_key,t),new_encrypted_message);
|
![]()
![]()
![]()
![]()
![]()
Now we see that the attack by LLL is no longer successful.
Assignment Four: little public keys are insecure
The public key is the vector of weights. With a public key of size 8, one could try all possible 256 = 2^8 combinations to decrypt one character. Trying one combination takes at most 7 additions. Assume that trying 7 additions takes only one millisecond, then it takes 256 milliseconds to decrypt one character.
| > | one_char := .256*seconds; |
| > | length(my_message)*one_char; |
So it would take less than a minute to decrypt my message.
Suppose our public key would be twice as long, i.e.: 16:
| > | one_char := evalf(2^16/1000)*seconds; |
| > | a := length(my_message)*one_char; |
| > | op(1,a)/(60*60)*hours; |
Decrypting our message would take longer, but not perhaps as long as we would like. Let us take 64 as size of the public key:
| > | one_char := evalf(2^64/1000)*seconds; |
| > | op(1,one_char)/(60*60*24*365)*years; |
| > |
Even decrypting as little as one character would take more than a million years.
To use a public key with 64 bits, the encode and decode operations would need to be changed. Instead of working with lists of 8 bits, we would need to encode our strings as lists of 64 bits each.