|
|
@@ -0,0 +1,305 @@ |
|
|
|
// package newhope contains a post-quantum key agreement algorithm, |
|
|
|
// reimplemented from the reference implementation at |
|
|
|
// https://github.com/tpoeppelmann/newhope. |
|
|
|
// |
|
|
|
// Note that this package does not interoperate with the reference |
|
|
|
// implementation. |
|
|
|
package newhope |
|
|
|
|
|
|
|
import ( |
|
|
|
"crypto/aes" |
|
|
|
"crypto/cipher" |
|
|
|
"errors" |
|
|
|
"io" |
|
|
|
) |
|
|
|
|
|
|
|
const ( |
|
|
|
// q is the prime that defines the field. |
|
|
|
q = 12289 |
|
|
|
// n is the number of coefficients in polynomials. |
|
|
|
n = 1024 |
|
|
|
// k is the width of the noise distribution. |
|
|
|
k = 16 |
|
|
|
|
|
|
|
// These values are used in the NTT calculation. See the paper for |
|
|
|
// details about their origins. |
|
|
|
omega = 49 |
|
|
|
invOmega = 1254 |
|
|
|
sqrtOmega = 7 |
|
|
|
invSqrtOmega = 8778 |
|
|
|
invN = 12277 |
|
|
|
|
|
|
|
// encodedPolyLen is the length, in bytes, of an encoded polynomial. The |
|
|
|
// encoding uses 14 bits per coefficient. |
|
|
|
encodedPolyLen = (n * 14) / 8 |
|
|
|
|
|
|
|
// offerMsgLen is the length, in bytes, of the offering (first) message of |
|
|
|
// the key exchange. |
|
|
|
OfferMsgLen = encodedPolyLen + 32 |
|
|
|
|
|
|
|
// acceptMsgLen is the length, in bytes, of the accepting (second) message |
|
|
|
// of the key exchange. |
|
|
|
AcceptMsgLen = encodedPolyLen + 256 |
|
|
|
) |
|
|
|
|
|
|
|
// count16Bits returns the number of '1' bits in v. |
|
|
|
func count16Bits(v uint16) (sum uint16) { |
|
|
|
for i := 0; i < 16; i++ { |
|
|
|
sum += v & 1 |
|
|
|
v >>= 1 |
|
|
|
} |
|
|
|
|
|
|
|
return sum |
|
|
|
} |
|
|
|
|
|
|
|
// Poly is a polynomial of n coefficients. |
|
|
|
type Poly [n]uint16 |
|
|
|
|
|
|
|
// Key is the result of a key agreement. |
|
|
|
type Key [32]uint8 |
|
|
|
|
|
|
|
// sampleNoise returns a random polynomial where the coefficients are |
|
|
|
// drawn from the noise distribution. |
|
|
|
func sampleNoise(rand io.Reader) *Poly { |
|
|
|
poly := new(Poly) |
|
|
|
buf := make([]byte, 4) |
|
|
|
|
|
|
|
for i := range poly { |
|
|
|
if _, err := io.ReadFull(rand, buf); err != nil { |
|
|
|
panic(err) |
|
|
|
} |
|
|
|
a := count16Bits(uint16(buf[0])<<8 | uint16(buf[1])) |
|
|
|
b := count16Bits(uint16(buf[2])<<8 | uint16(buf[3])) |
|
|
|
poly[i] = (q + a - b) % q |
|
|
|
} |
|
|
|
|
|
|
|
return poly |
|
|
|
} |
|
|
|
|
|
|
|
// randomPolynomial returns a random polynomial where the coefficients are |
|
|
|
// drawn uniformly at random from the underlying field. |
|
|
|
func randomPolynomial(rand io.Reader) *Poly { |
|
|
|
poly := new(Poly) |
|
|
|
|
|
|
|
buf := make([]byte, 2) |
|
|
|
for i := range poly { |
|
|
|
for { |
|
|
|
if _, err := io.ReadFull(rand, buf); err != nil { |
|
|
|
panic(err) |
|
|
|
} |
|
|
|
|
|
|
|
v := uint16(buf[1])<<8 | uint16(buf[0]) |
|
|
|
v &= 0x3fff |
|
|
|
|
|
|
|
if v < q { |
|
|
|
poly[i] = v |
|
|
|
break |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return poly |
|
|
|
} |
|
|
|
|
|
|
|
type zeroReader struct { |
|
|
|
io.Reader |
|
|
|
} |
|
|
|
|
|
|
|
func (z *zeroReader) Read(dst []byte) (n int, err error) { |
|
|
|
for i := range dst { |
|
|
|
dst[i] = 0 |
|
|
|
} |
|
|
|
return len(dst), nil |
|
|
|
} |
|
|
|
|
|
|
|
// seedToPolynomial uses AES-CTR to generate a pseudo-random polynomial given a |
|
|
|
// 32-byte seed. |
|
|
|
func seedToPolynomial(seed []byte) *Poly { |
|
|
|
aes, err := aes.NewCipher(seed[0:16]) |
|
|
|
if err != nil { |
|
|
|
panic(err) |
|
|
|
} |
|
|
|
stream := cipher.NewCTR(aes, seed[16:32]) |
|
|
|
reader := &cipher.StreamReader{S: stream, R: &zeroReader{}} |
|
|
|
return randomPolynomial(reader) |
|
|
|
} |
|
|
|
|
|
|
|
// forwardNTT converts |in| into the frequency domain. |
|
|
|
func forwardNTT(in *Poly) *Poly { |
|
|
|
return ntt(in, omega, sqrtOmega, 1, 1) |
|
|
|
} |
|
|
|
|
|
|
|
// inverseNTT converts |in| into the time domain. |
|
|
|
func inverseNTT(in *Poly) *Poly { |
|
|
|
return ntt(in, invOmega, 1, invSqrtOmega, invN) |
|
|
|
} |
|
|
|
|
|
|
|
// ntt performs the number-theoretic transform (a discrete Fourier transform in |
|
|
|
// a field) on in. Significant magic is in effect here. See the paper for the |
|
|
|
// details of how this works. |
|
|
|
func ntt(in *Poly, omega, preScaleBase, postScaleBase, postScale uint16) *Poly { |
|
|
|
out := new(Poly) |
|
|
|
omega_to_the_i := 1 |
|
|
|
|
|
|
|
for i := range out { |
|
|
|
omegaToTheIJ := 1 |
|
|
|
preScale := int(1) |
|
|
|
sum := 0 |
|
|
|
|
|
|
|
for j := range in { |
|
|
|
t := (int(in[j]) * preScale) % q |
|
|
|
sum += (t * omegaToTheIJ) % q |
|
|
|
omegaToTheIJ = (omegaToTheIJ * omega_to_the_i) % q |
|
|
|
preScale = (int(preScaleBase) * preScale) % q |
|
|
|
} |
|
|
|
|
|
|
|
out[i] = uint16((sum * int(postScale)) % q) |
|
|
|
|
|
|
|
omega_to_the_i = (omega_to_the_i * int(omega)) % q |
|
|
|
postScale = uint16((int(postScale) * int(postScaleBase)) % q) |
|
|
|
} |
|
|
|
|
|
|
|
return out |
|
|
|
} |
|
|
|
|
|
|
|
// encodeRec encodes the reconciliation data compactly, for use in the accept |
|
|
|
// message. |
|
|
|
func encodeRec(rec *reconciliationData) []byte { |
|
|
|
var ret [n / 4]byte |
|
|
|
|
|
|
|
for i := 0; i < n/4; i++ { |
|
|
|
ret[i] = rec[4*i] | rec[4*i+1]<<2 | rec[4*i+2]<<4 | rec[4*i+3]<<6 |
|
|
|
} |
|
|
|
|
|
|
|
return ret[:] |
|
|
|
} |
|
|
|
|
|
|
|
// decodeRec decodes reconciliation data from the accept message. |
|
|
|
func decodeRec(message []byte) (rec *reconciliationData) { |
|
|
|
rec = new(reconciliationData) |
|
|
|
|
|
|
|
for i, b := range message { |
|
|
|
rec[4*i] = b & 0x03 |
|
|
|
rec[4*i+1] = (b >> 2) & 0x3 |
|
|
|
rec[4*i+2] = (b >> 4) & 0x3 |
|
|
|
rec[4*i+3] = b >> 6 |
|
|
|
} |
|
|
|
|
|
|
|
return rec |
|
|
|
} |
|
|
|
|
|
|
|
// encodePoly returns a byte array that encodes a polynomial compactly, with 14 |
|
|
|
// bits per coefficient. |
|
|
|
func encodePoly(poly *Poly) []byte { |
|
|
|
ret := make([]byte, encodedPolyLen) |
|
|
|
|
|
|
|
for i := 0; i < n/4; i++ { |
|
|
|
t0 := poly[4*i] |
|
|
|
t1 := poly[4*i+1] |
|
|
|
t2 := poly[4*i+2] |
|
|
|
t3 := poly[4*i+3] |
|
|
|
|
|
|
|
ret[7*i] = byte(t0) |
|
|
|
ret[7*i+1] = byte(t0>>8) | byte(t1<<6) |
|
|
|
ret[7*i+2] = byte(t1 >> 2) |
|
|
|
ret[7*i+3] = byte(t1>>10) | byte(t2<<4) |
|
|
|
ret[7*i+4] = byte(t2 >> 4) |
|
|
|
ret[7*i+5] = byte(t2>>12) | byte(t3<<2) |
|
|
|
ret[7*i+6] = byte(t3 >> 6) |
|
|
|
} |
|
|
|
|
|
|
|
return ret |
|
|
|
} |
|
|
|
|
|
|
|
// decodePoly inverts encodePoly. |
|
|
|
func decodePoly(encoded []byte) *Poly { |
|
|
|
ret := new(Poly) |
|
|
|
|
|
|
|
for i := 0; i < n/4; i++ { |
|
|
|
ret[4*i] = uint16(encoded[7*i]) | uint16(encoded[7*i+1]&0x3f)<<8 |
|
|
|
ret[4*i+1] = uint16(encoded[7*i+1])>>6 | uint16(encoded[7*i+2])<<2 | uint16(encoded[7*i+3]&0x0f)<<10 |
|
|
|
ret[4*i+2] = uint16(encoded[7*i+3])>>4 | uint16(encoded[7*i+4])<<4 | uint16(encoded[7*i+5]&0x03)<<12 |
|
|
|
ret[4*i+3] = uint16(encoded[7*i+5])>>2 | uint16(encoded[7*i+6])<<6 |
|
|
|
} |
|
|
|
|
|
|
|
return ret |
|
|
|
} |
|
|
|
|
|
|
|
// Offer starts a new key exchange. It returns a message that should be |
|
|
|
// transmitted to the peer, and a polynomial that must be retained in order to |
|
|
|
// complete the exchange. |
|
|
|
func Offer(rand io.Reader) (offerMsg []byte, sFreq *Poly) { |
|
|
|
seed := make([]byte, 32) |
|
|
|
|
|
|
|
if _, err := io.ReadFull(rand, seed); err != nil { |
|
|
|
panic(err) |
|
|
|
} |
|
|
|
|
|
|
|
aFreq := seedToPolynomial(seed) |
|
|
|
sFreq = forwardNTT(sampleNoise(rand)) |
|
|
|
eFreq := forwardNTT(sampleNoise(rand)) |
|
|
|
|
|
|
|
bFreq := new(Poly) |
|
|
|
for i := range bFreq { |
|
|
|
bFreq[i] = uint16((int(sFreq[i])*int(aFreq[i]) + int(eFreq[i])) % q) |
|
|
|
} |
|
|
|
|
|
|
|
offerMsg = encodePoly(bFreq) |
|
|
|
offerMsg = append(offerMsg, seed[:]...) |
|
|
|
return offerMsg, sFreq |
|
|
|
} |
|
|
|
|
|
|
|
// Accept processes a message generated by |Offer| and returns a reply message |
|
|
|
// and the shared key. |
|
|
|
func Accept(rand io.Reader, offerMsg []byte) (sharedKey Key, acceptMsg []byte, err error) { |
|
|
|
if len(offerMsg) != OfferMsgLen { |
|
|
|
return sharedKey, nil, errors.New("newhope: offer message has incorrect length") |
|
|
|
} |
|
|
|
|
|
|
|
bFreq := decodePoly(offerMsg) |
|
|
|
seed := offerMsg[encodedPolyLen:] |
|
|
|
|
|
|
|
aFreq := seedToPolynomial(seed) |
|
|
|
sPrimeFreq := forwardNTT(sampleNoise(rand)) |
|
|
|
ePrimeFreq := forwardNTT(sampleNoise(rand)) |
|
|
|
|
|
|
|
uFreq := new(Poly) |
|
|
|
for i := range uFreq { |
|
|
|
uFreq[i] = uint16((int(sPrimeFreq[i])*int(aFreq[i]) + int(ePrimeFreq[i])) % q) |
|
|
|
} |
|
|
|
|
|
|
|
vFreq := new(Poly) |
|
|
|
for i := range vFreq { |
|
|
|
vFreq[i] = uint16((int(sPrimeFreq[i]) * int(bFreq[i])) % q) |
|
|
|
} |
|
|
|
|
|
|
|
v := inverseNTT(vFreq) |
|
|
|
ePrimePrime := sampleNoise(rand) |
|
|
|
for i := range v { |
|
|
|
v[i] = uint16((int(v[i]) + int(ePrimePrime[i])) % q) |
|
|
|
} |
|
|
|
|
|
|
|
rec := helprec(rand, v) |
|
|
|
|
|
|
|
sharedKey = reconcile(v, rec) |
|
|
|
acceptMsg = encodePoly(uFreq) |
|
|
|
acceptMsg = append(acceptMsg, encodeRec(rec)[:]...) |
|
|
|
return sharedKey, acceptMsg, nil |
|
|
|
} |
|
|
|
|
|
|
|
// Finish processes the reply from the peer and returns the shared key. |
|
|
|
func (sk *Poly) Finish(acceptMsg []byte) (sharedKey Key, err error) { |
|
|
|
if len(acceptMsg) != AcceptMsgLen { |
|
|
|
return sharedKey, errors.New("newhope: accept message has incorrect length") |
|
|
|
} |
|
|
|
|
|
|
|
uFreq := decodePoly(acceptMsg[:encodedPolyLen]) |
|
|
|
rec := decodeRec(acceptMsg[encodedPolyLen:]) |
|
|
|
|
|
|
|
for i, u := range uFreq { |
|
|
|
uFreq[i] = uint16((int(u) * int(sk[i])) % q) |
|
|
|
} |
|
|
|
u := inverseNTT(uFreq) |
|
|
|
|
|
|
|
return reconcile(u, rec), nil |
|
|
|
} |