diff --git a/.gitignore b/.gitignore index 2e6dc99605f..861f2355d39 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,6 @@ _testmain.go *.exe *.test *.prof -flymake* \ No newline at end of file +flymake* + +QR.png \ No newline at end of file diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index bb83933db6f..860788b9353 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -15,6 +15,16 @@ "Comment": "null-236", "Rev": "69e2a90ed92d03812364aeb947b7068dc42e561e" }, + { + "ImportPath": "code.google.com/p/rsc/gf256", + "Comment": "null-258", + "Rev": "2d8aa6027fab93979a3b4ecdbeb1ba430b489318" + }, + { + "ImportPath": "code.google.com/p/rsc/qr", + "Comment": "null-258", + "Rev": "2d8aa6027fab93979a3b4ecdbeb1ba430b489318" + }, { "ImportPath": "github.com/alecthomas/template", "Rev": "b867cc6ab45cece8143cfcc6fc9c77cf3f2c23c0" @@ -51,6 +61,10 @@ "Comment": "v0.4.6-6-g4734e7a", "Rev": "4734e7aca379f0d7fcdf04fbb2101696a4b45ce8" }, + { + "ImportPath": "github.com/gokyle/hotp", + "Rev": "cab5c36e945092b6fb703b6c785bb06fd0537a0d" + }, { "ImportPath": "github.com/gravitational/configure", "Rev": "725b3af8847feced5316be064a297d7c8a1052b3" @@ -63,10 +77,6 @@ "ImportPath": "github.com/gravitational/log", "Rev": "8e1bbf1bba0515719dbb17adf1da20b5390ac8da" }, - { - "ImportPath": "github.com/gravitational/orbit/lib/utils", - "Rev": "fe21737572762319f5e106adf361eac625ca1bc7" - }, { "ImportPath": "github.com/gravitational/roundtrip", "Rev": "e7bee8354256fe4ae0d847e4c5d946d3e86f22af" diff --git a/Godeps/_workspace/src/code.google.com/p/rsc/gf256/blog_test.go b/Godeps/_workspace/src/code.google.com/p/rsc/gf256/blog_test.go new file mode 100644 index 00000000000..4fbf7ecba89 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/rsc/gf256/blog_test.go @@ -0,0 +1,85 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file contains a straightforward implementation of +// Reed-Solomon encoding, along with a benchmark. +// It goes with http://research.swtch.com/field. +// +// For an optimized implementation, see gf256.go. + +package gf256 + +import ( + "bytes" + "fmt" + "testing" +) + +// BlogECC writes to check the error correcting code bytes +// for data using the given Reed-Solomon parameters. +func BlogECC(rs *RSEncoder, m []byte, check []byte) { + if len(check) < rs.c { + panic("gf256: invalid check byte length") + } + if rs.c == 0 { + return + } + + // The check bytes are the remainder after dividing + // data padded with c zeros by the generator polynomial. + + // p = data padded with c zeros. + var p []byte + n := len(m) + rs.c + if len(rs.p) >= n { + p = rs.p + } else { + p = make([]byte, n) + } + copy(p, m) + for i := len(m); i < len(p); i++ { + p[i] = 0 + } + + gen := rs.gen + + // Divide p by gen, leaving the remainder in p[len(data):]. + // p[0] is the most significant term in p, and + // gen[0] is the most significant term in the generator. + for i := 0; i < len(m); i++ { + k := f.Mul(p[i], f.Inv(gen[0])) // k = pi / g0 + // p -= k·g + for j, g := range gen { + p[i+j] = f.Add(p[i+j], f.Mul(k, g)) + } + } + + copy(check, p[len(m):]) + rs.p = p +} + +func BenchmarkBlogECC(b *testing.B) { + data := []byte{0x10, 0x20, 0x0c, 0x56, 0x61, 0x80, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0x10, 0x20, 0x0c, 0x56, 0x61, 0x80, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11} + check := []byte{0x29, 0x41, 0xb3, 0x93, 0x8, 0xe8, 0xa3, 0xe7, 0x63, 0x8f} + out := make([]byte, len(check)) + rs := NewRSEncoder(f, len(check)) + for i := 0; i < b.N; i++ { + BlogECC(rs, data, out) + } + b.SetBytes(int64(len(data))) + if !bytes.Equal(out, check) { + fmt.Printf("have %#v want %#v\n", out, check) + } +} + +func TestBlogECC(t *testing.T) { + data := []byte{0x10, 0x20, 0x0c, 0x56, 0x61, 0x80, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11} + check := []byte{0xa5, 0x24, 0xd4, 0xc1, 0xed, 0x36, 0xc7, 0x87, 0x2c, 0x55} + out := make([]byte, len(check)) + rs := NewRSEncoder(f, len(check)) + BlogECC(rs, data, out) + if !bytes.Equal(out, check) { + t.Errorf("have %x want %x", out, check) + } +} diff --git a/Godeps/_workspace/src/code.google.com/p/rsc/gf256/gf256.go b/Godeps/_workspace/src/code.google.com/p/rsc/gf256/gf256.go new file mode 100644 index 00000000000..bfeeeb3afd2 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/rsc/gf256/gf256.go @@ -0,0 +1,241 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package gf256 implements arithmetic over the Galois Field GF(256). +package gf256 + +import "strconv" + +// A Field represents an instance of GF(256) defined by a specific polynomial. +type Field struct { + log [256]byte // log[0] is unused + exp [510]byte +} + +// NewField returns a new field corresponding to the polynomial poly +// and generator α. The Reed-Solomon encoding in QR codes uses +// polynomial 0x11d with generator 2. +// +// The choice of generator α only affects the Exp and Log operations. +func NewField(poly, α int) *Field { + if poly < 0x100 || poly >= 0x200 || reducible(poly) { + panic("gf256: invalid polynomial: " + strconv.Itoa(poly)) + } + + var f Field + x := 1 + for i := 0; i < 255; i++ { + if x == 1 && i != 0 { + panic("gf256: invalid generator " + strconv.Itoa(α) + + " for polynomial " + strconv.Itoa(poly)) + } + f.exp[i] = byte(x) + f.exp[i+255] = byte(x) + f.log[x] = byte(i) + x = mul(x, α, poly) + } + f.log[0] = 255 + for i := 0; i < 255; i++ { + if f.log[f.exp[i]] != byte(i) { + panic("bad log") + } + if f.log[f.exp[i+255]] != byte(i) { + panic("bad log") + } + } + for i := 1; i < 256; i++ { + if f.exp[f.log[i]] != byte(i) { + panic("bad log") + } + } + + return &f +} + +// nbit returns the number of significant in p. +func nbit(p int) uint { + n := uint(0) + for ; p > 0; p >>= 1 { + n++ + } + return n +} + +// polyDiv divides the polynomial p by q and returns the remainder. +func polyDiv(p, q int) int { + np := nbit(p) + nq := nbit(q) + for ; np >= nq; np-- { + if p&(1<<(np-1)) != 0 { + p ^= q << (np - nq) + } + } + return p +} + +// mul returns the product x*y mod poly, a GF(256) multiplication. +func mul(x, y, poly int) int { + z := 0 + for x > 0 { + if x&1 != 0 { + z ^= y + } + x >>= 1 + y <<= 1 + if y&0x100 != 0 { + y ^= poly + } + } + return z +} + +// reducible reports whether p is reducible. +func reducible(p int) bool { + // Multiplying n-bit * n-bit produces (2n-1)-bit, + // so if p is reducible, one of its factors must be + // of np/2+1 bits or fewer. + np := nbit(p) + for q := 2; q < 1<<(np/2+1); q++ { + if polyDiv(p, q) == 0 { + return true + } + } + return false +} + +// Add returns the sum of x and y in the field. +func (f *Field) Add(x, y byte) byte { + return x ^ y +} + +// Exp returns the base-α exponential of e in the field. +// If e < 0, Exp returns 0. +func (f *Field) Exp(e int) byte { + if e < 0 { + return 0 + } + return f.exp[e%255] +} + +// Log returns the base-α logarithm of x in the field. +// If x == 0, Log returns -1. +func (f *Field) Log(x byte) int { + if x == 0 { + return -1 + } + return int(f.log[x]) +} + +// Inv returns the multiplicative inverse of x in the field. +// If x == 0, Inv returns 0. +func (f *Field) Inv(x byte) byte { + if x == 0 { + return 0 + } + return f.exp[255-f.log[x]] +} + +// Mul returns the product of x and y in the field. +func (f *Field) Mul(x, y byte) byte { + if x == 0 || y == 0 { + return 0 + } + return f.exp[int(f.log[x])+int(f.log[y])] +} + +// An RSEncoder implements Reed-Solomon encoding +// over a given field using a given number of error correction bytes. +type RSEncoder struct { + f *Field + c int + gen []byte + lgen []byte + p []byte +} + +func (f *Field) gen(e int) (gen, lgen []byte) { + // p = 1 + p := make([]byte, e+1) + p[e] = 1 + + for i := 0; i < e; i++ { + // p *= (x + Exp(i)) + // p[j] = p[j]*Exp(i) + p[j+1]. + c := f.Exp(i) + for j := 0; j < e; j++ { + p[j] = f.Mul(p[j], c) ^ p[j+1] + } + p[e] = f.Mul(p[e], c) + } + + // lp = log p. + lp := make([]byte, e+1) + for i, c := range p { + if c == 0 { + lp[i] = 255 + } else { + lp[i] = byte(f.Log(c)) + } + } + + return p, lp +} + +// NewRSEncoder returns a new Reed-Solomon encoder +// over the given field and number of error correction bytes. +func NewRSEncoder(f *Field, c int) *RSEncoder { + gen, lgen := f.gen(c) + return &RSEncoder{f: f, c: c, gen: gen, lgen: lgen} +} + +// ECC writes to check the error correcting code bytes +// for data using the given Reed-Solomon parameters. +func (rs *RSEncoder) ECC(data []byte, check []byte) { + if len(check) < rs.c { + panic("gf256: invalid check byte length") + } + if rs.c == 0 { + return + } + + // The check bytes are the remainder after dividing + // data padded with c zeros by the generator polynomial. + + // p = data padded with c zeros. + var p []byte + n := len(data) + rs.c + if len(rs.p) >= n { + p = rs.p + } else { + p = make([]byte, n) + } + copy(p, data) + for i := len(data); i < len(p); i++ { + p[i] = 0 + } + + // Divide p by gen, leaving the remainder in p[len(data):]. + // p[0] is the most significant term in p, and + // gen[0] is the most significant term in the generator, + // which is always 1. + // To avoid repeated work, we store various values as + // lv, not v, where lv = log[v]. + f := rs.f + lgen := rs.lgen[1:] + for i := 0; i < len(data); i++ { + c := p[i] + if c == 0 { + continue + } + q := p[i+1:] + exp := f.exp[f.log[c]:] + for j, lg := range lgen { + if lg != 255 { // lgen uses 255 for log 0 + q[j] ^= exp[lg] + } + } + } + copy(check, p[len(data):]) + rs.p = p +} diff --git a/Godeps/_workspace/src/code.google.com/p/rsc/gf256/gf256_test.go b/Godeps/_workspace/src/code.google.com/p/rsc/gf256/gf256_test.go new file mode 100644 index 00000000000..f77fa7d67b6 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/rsc/gf256/gf256_test.go @@ -0,0 +1,194 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package gf256 + +import ( + "bytes" + "fmt" + "testing" +) + +var f = NewField(0x11d, 2) // x^8 + x^4 + x^3 + x^2 + 1 + +func TestBasic(t *testing.T) { + if f.Exp(0) != 1 || f.Exp(1) != 2 || f.Exp(255) != 1 { + panic("bad Exp") + } +} + +func TestECC(t *testing.T) { + data := []byte{0x10, 0x20, 0x0c, 0x56, 0x61, 0x80, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11} + check := []byte{0xa5, 0x24, 0xd4, 0xc1, 0xed, 0x36, 0xc7, 0x87, 0x2c, 0x55} + out := make([]byte, len(check)) + rs := NewRSEncoder(f, len(check)) + rs.ECC(data, out) + if !bytes.Equal(out, check) { + t.Errorf("have %x want %x", out, check) + } +} + +func TestLinear(t *testing.T) { + d1 := []byte{0x00, 0x00} + c1 := []byte{0x00, 0x00} + out := make([]byte, len(c1)) + rs := NewRSEncoder(f, len(c1)) + if rs.ECC(d1, out); !bytes.Equal(out, c1) { + t.Errorf("ECBytes(%x, %d) = %x, want 0", d1, len(c1), out) + } + d2 := []byte{0x00, 0x01} + c2 := make([]byte, 2) + rs.ECC(d2, c2) + d3 := []byte{0x00, 0x02} + c3 := make([]byte, 2) + rs.ECC(d3, c3) + cx := make([]byte, 2) + for i := range cx { + cx[i] = c2[i] ^ c3[i] + } + d4 := []byte{0x00, 0x03} + c4 := make([]byte, 2) + rs.ECC(d4, c4) + if !bytes.Equal(cx, c4) { + t.Errorf("ECBytes(%x, 2) = %x\nECBytes(%x, 2) = %x\nxor = %x\nECBytes(%x, 2) = %x", + d2, c2, d3, c3, cx, d4, c4) + } +} + +func TestGaussJordan(t *testing.T) { + rs := NewRSEncoder(f, 2) + m := make([][]byte, 16) + for i := range m { + m[i] = make([]byte, 4) + m[i][i/8] = 1 << uint(i%8) + rs.ECC(m[i][:2], m[i][2:]) + } + if false { + fmt.Printf("---\n") + for _, row := range m { + fmt.Printf("%x\n", row) + } + } + b := []uint{0, 1, 2, 3, 12, 13, 14, 15, 20, 21, 22, 23, 24, 25, 26, 27} + for i := 0; i < 16; i++ { + bi := b[i] + if m[i][bi/8]&(1<<(7-bi%8)) == 0 { + for j := i + 1; ; j++ { + if j >= len(m) { + t.Errorf("lost track for %d", bi) + break + } + if m[j][bi/8]&(1<<(7-bi%8)) != 0 { + m[i], m[j] = m[j], m[i] + break + } + } + } + for j := i + 1; j < len(m); j++ { + if m[j][bi/8]&(1<<(7-bi%8)) != 0 { + for k := range m[j] { + m[j][k] ^= m[i][k] + } + } + } + } + if false { + fmt.Printf("---\n") + for _, row := range m { + fmt.Printf("%x\n", row) + } + } + for i := 15; i >= 0; i-- { + bi := b[i] + for j := i - 1; j >= 0; j-- { + if m[j][bi/8]&(1<<(7-bi%8)) != 0 { + for k := range m[j] { + m[j][k] ^= m[i][k] + } + } + } + } + if false { + fmt.Printf("---\n") + for _, row := range m { + fmt.Printf("%x", row) + out := make([]byte, 2) + if rs.ECC(row[:2], out); !bytes.Equal(out, row[2:]) { + fmt.Printf(" - want %x", out) + } + fmt.Printf("\n") + } + } +} + +func BenchmarkECC(b *testing.B) { + data := []byte{0x10, 0x20, 0x0c, 0x56, 0x61, 0x80, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0x10, 0x20, 0x0c, 0x56, 0x61, 0x80, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11} + check := []byte{0x29, 0x41, 0xb3, 0x93, 0x8, 0xe8, 0xa3, 0xe7, 0x63, 0x8f} + out := make([]byte, len(check)) + rs := NewRSEncoder(f, len(check)) + for i := 0; i < b.N; i++ { + rs.ECC(data, out) + } + b.SetBytes(int64(len(data))) + if !bytes.Equal(out, check) { + fmt.Printf("have %#v want %#v\n", out, check) + } +} + +func TestGen(t *testing.T) { + for i := 0; i < 256; i++ { + _, lg := f.gen(i) + if lg[0] != 0 { + t.Errorf("#%d: %x", i, lg) + } + } +} + +func TestReducible(t *testing.T) { + var count = []int{1, 2, 3, 6, 9, 18, 30, 56, 99, 186} // oeis.org/A1037 + for i, want := range count { + n := 0 + for p := 1 << uint(i+2); p < 1< 0 { + n := nbit + if n > 8 { + n = 8 + } + if b.nbit%8 == 0 { + b.b = append(b.b, 0) + } else { + m := -b.nbit & 7 + if n > m { + n = m + } + } + b.nbit += n + sh := uint(nbit - n) + b.b[len(b.b)-1] |= uint8(v >> sh << uint(-b.nbit&7)) + v -= v >> sh << sh + nbit -= n + } +} + +// Num is the encoding for numeric data. +// The only valid characters are the decimal digits 0 through 9. +type Num string + +func (s Num) String() string { + return fmt.Sprintf("Num(%#q)", string(s)) +} + +func (s Num) Check() error { + for _, c := range s { + if c < '0' || '9' < c { + return fmt.Errorf("non-numeric string %#q", string(s)) + } + } + return nil +} + +var numLen = [3]int{10, 12, 14} + +func (s Num) Bits(v Version) int { + return 4 + numLen[v.sizeClass()] + (10*len(s)+2)/3 +} + +func (s Num) Encode(b *Bits, v Version) { + b.Write(1, 4) + b.Write(uint(len(s)), numLen[v.sizeClass()]) + var i int + for i = 0; i+3 <= len(s); i += 3 { + w := uint(s[i]-'0')*100 + uint(s[i+1]-'0')*10 + uint(s[i+2]-'0') + b.Write(w, 10) + } + switch len(s) - i { + case 1: + w := uint(s[i] - '0') + b.Write(w, 4) + case 2: + w := uint(s[i]-'0')*10 + uint(s[i+1]-'0') + b.Write(w, 7) + } +} + +// Alpha is the encoding for alphanumeric data. +// The valid characters are 0-9A-Z$%*+-./: and space. +type Alpha string + +const alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:" + +func (s Alpha) String() string { + return fmt.Sprintf("Alpha(%#q)", string(s)) +} + +func (s Alpha) Check() error { + for _, c := range s { + if strings.IndexRune(alphabet, c) < 0 { + return fmt.Errorf("non-alphanumeric string %#q", string(s)) + } + } + return nil +} + +var alphaLen = [3]int{9, 11, 13} + +func (s Alpha) Bits(v Version) int { + return 4 + alphaLen[v.sizeClass()] + (11*len(s)+1)/2 +} + +func (s Alpha) Encode(b *Bits, v Version) { + b.Write(2, 4) + b.Write(uint(len(s)), alphaLen[v.sizeClass()]) + var i int + for i = 0; i+2 <= len(s); i += 2 { + w := uint(strings.IndexRune(alphabet, rune(s[i])))*45 + + uint(strings.IndexRune(alphabet, rune(s[i+1]))) + b.Write(w, 11) + } + + if i < len(s) { + w := uint(strings.IndexRune(alphabet, rune(s[i]))) + b.Write(w, 6) + } +} + +// String is the encoding for 8-bit data. All bytes are valid. +type String string + +func (s String) String() string { + return fmt.Sprintf("String(%#q)", string(s)) +} + +func (s String) Check() error { + return nil +} + +var stringLen = [3]int{8, 16, 16} + +func (s String) Bits(v Version) int { + return 4 + stringLen[v.sizeClass()] + 8*len(s) +} + +func (s String) Encode(b *Bits, v Version) { + b.Write(4, 4) + b.Write(uint(len(s)), stringLen[v.sizeClass()]) + for i := 0; i < len(s); i++ { + b.Write(uint(s[i]), 8) + } +} + +// A Pixel describes a single pixel in a QR code. +type Pixel uint32 + +const ( + Black Pixel = 1 << iota + Invert +) + +func (p Pixel) Offset() uint { + return uint(p >> 6) +} + +func OffsetPixel(o uint) Pixel { + return Pixel(o << 6) +} + +func (r PixelRole) Pixel() Pixel { + return Pixel(r << 2) +} + +func (p Pixel) Role() PixelRole { + return PixelRole(p>>2) & 15 +} + +func (p Pixel) String() string { + s := p.Role().String() + if p&Black != 0 { + s += "+black" + } + if p&Invert != 0 { + s += "+invert" + } + s += "+" + strconv.FormatUint(uint64(p.Offset()), 10) + return s +} + +// A PixelRole describes the role of a QR pixel. +type PixelRole uint32 + +const ( + _ PixelRole = iota + Position // position squares (large) + Alignment // alignment squares (small) + Timing // timing strip between position squares + Format // format metadata + PVersion // version pattern + Unused // unused pixel + Data // data bit + Check // error correction check bit + Extra +) + +var roles = []string{ + "", + "position", + "alignment", + "timing", + "format", + "pversion", + "unused", + "data", + "check", + "extra", +} + +func (r PixelRole) String() string { + if Position <= r && r <= Check { + return roles[r] + } + return strconv.Itoa(int(r)) +} + +// A Level represents a QR error correction level. +// From least to most tolerant of errors, they are L, M, Q, H. +type Level int + +const ( + L Level = iota + M + Q + H +) + +func (l Level) String() string { + if L <= l && l <= H { + return "LMQH"[l : l+1] + } + return strconv.Itoa(int(l)) +} + +// A Code is a square pixel grid. +type Code struct { + Bitmap []byte // 1 is black, 0 is white + Size int // number of pixels on a side + Stride int // number of bytes per row +} + +func (c *Code) Black(x, y int) bool { + return 0 <= x && x < c.Size && 0 <= y && y < c.Size && + c.Bitmap[y*c.Stride+x/8]&(1<= pad { + break + } + b.Write(0x11, 8) + } + } +} + +func (b *Bits) AddCheckBytes(v Version, l Level) { + nd := v.DataBytes(l) + if b.nbit < nd*8 { + b.Pad(nd*8 - b.nbit) + } + if b.nbit != nd*8 { + panic("qr: too much data") + } + + dat := b.Bytes() + vt := &vtab[v] + lev := &vt.level[l] + db := nd / lev.nblock + extra := nd % lev.nblock + chk := make([]byte, lev.check) + rs := gf256.NewRSEncoder(Field, lev.check) + for i := 0; i < lev.nblock; i++ { + if i == lev.nblock-extra { + db++ + } + rs.ECC(dat[:db], chk) + b.Append(chk) + dat = dat[db:] + } + + if len(b.Bytes()) != vt.bytes { + panic("qr: internal error") + } +} + +func (p *Plan) Encode(text ...Encoding) (*Code, error) { + var b Bits + for _, t := range text { + if err := t.Check(); err != nil { + return nil, err + } + t.Encode(&b, p.Version) + } + if b.Bits() > p.DataBytes*8 { + return nil, fmt.Errorf("cannot encode %d bits into %d-bit code", b.Bits(), p.DataBytes*8) + } + b.AddCheckBytes(p.Version, p.Level) + bytes := b.Bytes() + + // Now we have the checksum bytes and the data bytes. + // Construct the actual code. + c := &Code{Size: len(p.Pixel), Stride: (len(p.Pixel) + 7) &^ 7} + c.Bitmap = make([]byte, c.Stride*c.Size) + crow := c.Bitmap + for _, row := range p.Pixel { + for x, pix := range row { + switch pix.Role() { + case Data, Check: + o := pix.Offset() + if bytes[o/8]&(1< 40 { + return nil, fmt.Errorf("invalid QR version %d", int(v)) + } + siz := 17 + int(v)*4 + m := grid(siz) + p.Pixel = m + + // Timing markers (overwritten by boxes). + const ti = 6 // timing is in row/column 6 (counting from 0) + for i := range m { + p := Timing.Pixel() + if i&1 == 0 { + p |= Black + } + m[i][ti] = p + m[ti][i] = p + } + + // Position boxes. + posBox(m, 0, 0) + posBox(m, siz-7, 0) + posBox(m, 0, siz-7) + + // Alignment boxes. + info := &vtab[v] + for x := 4; x+5 < siz; { + for y := 4; y+5 < siz; { + // don't overwrite timing markers + if (x < 7 && y < 7) || (x < 7 && y+5 >= siz-7) || (x+5 >= siz-7 && y < 7) { + } else { + alignBox(m, x, y) + } + if y == 4 { + y = info.apos + } else { + y += info.astride + } + } + if x == 4 { + x = info.apos + } else { + x += info.astride + } + } + + // Version pattern. + pat := vtab[v].pattern + if pat != 0 { + v := pat + for x := 0; x < 6; x++ { + for y := 0; y < 3; y++ { + p := PVersion.Pixel() + if v&1 != 0 { + p |= Black + } + m[siz-11+y][x] = p + m[x][siz-11+y] = p + v >>= 1 + } + } + } + + // One lonely black pixel + m[siz-8][8] = Unused.Pixel() | Black + + return p, nil +} + +// fplan adds the format pixels +func fplan(l Level, m Mask, p *Plan) error { + // Format pixels. + fb := uint32(l^1) << 13 // level: L=01, M=00, Q=11, H=10 + fb |= uint32(m) << 10 // mask + const formatPoly = 0x537 + rem := fb + for i := 14; i >= 10; i-- { + if rem&(1<>i)&1 == 1 { + pix |= Black + } + if (invert>>i)&1 == 1 { + pix ^= Invert | Black + } + // top left + switch { + case i < 6: + p.Pixel[i][8] = pix + case i < 8: + p.Pixel[i+1][8] = pix + case i < 9: + p.Pixel[8][7] = pix + default: + p.Pixel[8][14-i] = pix + } + // bottom right + switch { + case i < 8: + p.Pixel[8][siz-1-int(i)] = pix + default: + p.Pixel[siz-1-int(14-i)][8] = pix + } + } + return nil +} + +// lplan edits a version-only Plan to add information +// about the error correction levels. +func lplan(v Version, l Level, p *Plan) error { + p.Level = l + + nblock := vtab[v].level[l].nblock + ne := vtab[v].level[l].check + nde := (vtab[v].bytes - ne*nblock) / nblock + extra := (vtab[v].bytes - ne*nblock) % nblock + dataBits := (nde*nblock + extra) * 8 + checkBits := ne * nblock * 8 + + p.DataBytes = vtab[v].bytes - ne*nblock + p.CheckBytes = ne * nblock + p.Blocks = nblock + + // Make data + checksum pixels. + data := make([]Pixel, dataBits) + for i := range data { + data[i] = Data.Pixel() | OffsetPixel(uint(i)) + } + check := make([]Pixel, checkBits) + for i := range check { + check[i] = Check.Pixel() | OffsetPixel(uint(i+dataBits)) + } + + // Split into blocks. + dataList := make([][]Pixel, nblock) + checkList := make([][]Pixel, nblock) + for i := 0; i < nblock; i++ { + // The last few blocks have an extra data byte (8 pixels). + nd := nde + if i >= nblock-extra { + nd++ + } + dataList[i], data = data[0:nd*8], data[nd*8:] + checkList[i], check = check[0:ne*8], check[ne*8:] + } + if len(data) != 0 || len(check) != 0 { + panic("data/check math") + } + + // Build up bit sequence, taking first byte of each block, + // then second byte, and so on. Then checksums. + bits := make([]Pixel, dataBits+checkBits) + dst := bits + for i := 0; i < nde+1; i++ { + for _, b := range dataList { + if i*8 < len(b) { + copy(dst, b[i*8:(i+1)*8]) + dst = dst[8:] + } + } + } + for i := 0; i < ne; i++ { + for _, b := range checkList { + if i*8 < len(b) { + copy(dst, b[i*8:(i+1)*8]) + dst = dst[8:] + } + } + } + if len(dst) != 0 { + panic("dst math") + } + + // Sweep up pair of columns, + // then down, assigning to right then left pixel. + // Repeat. + // See Figure 2 of http://www.pclviewer.com/rs2/qrtopology.htm + siz := len(p.Pixel) + rem := make([]Pixel, 7) + for i := range rem { + rem[i] = Extra.Pixel() + } + src := append(bits, rem...) + for x := siz; x > 0; { + for y := siz - 1; y >= 0; y-- { + if p.Pixel[y][x-1].Role() == 0 { + p.Pixel[y][x-1], src = src[0], src[1:] + } + if p.Pixel[y][x-2].Role() == 0 { + p.Pixel[y][x-2], src = src[0], src[1:] + } + } + x -= 2 + if x == 7 { // vertical timing strip + x-- + } + for y := 0; y < siz; y++ { + if p.Pixel[y][x-1].Role() == 0 { + p.Pixel[y][x-1], src = src[0], src[1:] + } + if p.Pixel[y][x-2].Role() == 0 { + p.Pixel[y][x-2], src = src[0], src[1:] + } + } + x -= 2 + } + return nil +} + +// mplan edits a version+level-only Plan to add the mask. +func mplan(m Mask, p *Plan) error { + p.Mask = m + for y, row := range p.Pixel { + for x, pix := range row { + if r := pix.Role(); (r == Data || r == Check || r == Extra) && p.Mask.Invert(y, x) { + row[x] ^= Black | Invert + } + } + } + return nil +} + +// posBox draws a position (large) box at upper left x, y. +func posBox(m [][]Pixel, x, y int) { + pos := Position.Pixel() + // box + for dy := 0; dy < 7; dy++ { + for dx := 0; dx < 7; dx++ { + p := pos + if dx == 0 || dx == 6 || dy == 0 || dy == 6 || 2 <= dx && dx <= 4 && 2 <= dy && dy <= 4 { + p |= Black + } + m[y+dy][x+dx] = p + } + } + // white border + for dy := -1; dy < 8; dy++ { + if 0 <= y+dy && y+dy < len(m) { + if x > 0 { + m[y+dy][x-1] = pos + } + if x+7 < len(m) { + m[y+dy][x+7] = pos + } + } + } + for dx := -1; dx < 8; dx++ { + if 0 <= x+dx && x+dx < len(m) { + if y > 0 { + m[y-1][x+dx] = pos + } + if y+7 < len(m) { + m[y+7][x+dx] = pos + } + } + } +} + +// alignBox draw an alignment (small) box at upper left x, y. +func alignBox(m [][]Pixel, x, y int) { + // box + align := Alignment.Pixel() + for dy := 0; dy < 5; dy++ { + for dx := 0; dx < 5; dx++ { + p := align + if dx == 0 || dx == 4 || dy == 0 || dy == 4 || dx == 2 && dy == 2 { + p |= Black + } + m[y+dy][x+dx] = p + } + } +} diff --git a/Godeps/_workspace/src/code.google.com/p/rsc/qr/coding/qr_test.go b/Godeps/_workspace/src/code.google.com/p/rsc/qr/coding/qr_test.go new file mode 100644 index 00000000000..92237216b3c --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/rsc/qr/coding/qr_test.go @@ -0,0 +1,133 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package coding + +import ( + "bytes" + "testing" + + "github.com/gravitational/teleport/Godeps/_workspace/src/code.google.com/p/rsc/gf256" + "github.com/gravitational/teleport/Godeps/_workspace/src/code.google.com/p/rsc/qr/libqrencode" +) + +func test(t *testing.T, v Version, l Level, text ...Encoding) bool { + s := "" + ty := libqrencode.EightBit + switch x := text[0].(type) { + case String: + s = string(x) + case Alpha: + s = string(x) + ty = libqrencode.Alphanumeric + case Num: + s = string(x) + ty = libqrencode.Numeric + } + key, err := libqrencode.Encode(libqrencode.Version(v), libqrencode.Level(l), ty, s) + if err != nil { + t.Errorf("libqrencode.Encode(%v, %v, %d, %#q): %v", v, l, ty, s, err) + return false + } + mask := (^key.Pixel[8][2]&1)<<2 | (key.Pixel[8][3]&1)<<1 | (^key.Pixel[8][4] & 1) + p, err := NewPlan(v, l, Mask(mask)) + if err != nil { + t.Errorf("NewPlan(%v, L, %d): %v", v, err, mask) + return false + } + if len(p.Pixel) != len(key.Pixel) { + t.Errorf("%v: NewPlan uses %dx%d, libqrencode uses %dx%d", v, len(p.Pixel), len(p.Pixel), len(key.Pixel), len(key.Pixel)) + return false + } + c, err := p.Encode(text...) + if err != nil { + t.Errorf("Encode: %v", err) + return false + } + badpix := 0 +Pixel: + for y, prow := range p.Pixel { + for x, pix := range prow { + pix &^= Black + if c.Black(x, y) { + pix |= Black + } + + keypix := key.Pixel[y][x] + want := Pixel(0) + switch { + case keypix&libqrencode.Finder != 0: + want = Position.Pixel() + case keypix&libqrencode.Alignment != 0: + want = Alignment.Pixel() + case keypix&libqrencode.Timing != 0: + want = Timing.Pixel() + case keypix&libqrencode.Format != 0: + want = Format.Pixel() + want |= OffsetPixel(pix.Offset()) // sic + want |= pix & Invert + case keypix&libqrencode.PVersion != 0: + want = PVersion.Pixel() + case keypix&libqrencode.DataECC != 0: + if pix.Role() == Check || pix.Role() == Extra { + want = pix.Role().Pixel() + } else { + want = Data.Pixel() + } + want |= OffsetPixel(pix.Offset()) + want |= pix & Invert + default: + want = Unused.Pixel() + } + if keypix&libqrencode.Black != 0 { + want |= Black + } + if pix != want { + t.Errorf("%v/%v: Pixel[%d][%d] = %v, want %v %#x", v, mask, y, x, pix, want, keypix) + if badpix++; badpix >= 100 { + t.Errorf("stopping after %d bad pixels", badpix) + break Pixel + } + } + } + } + return badpix == 0 +} + +var input = []Encoding{ + String("hello"), + Num("1"), + Num("12"), + Num("123"), + Alpha("AB"), + Alpha("ABC"), +} + +func TestVersion(t *testing.T) { + badvers := 0 +Version: + for v := Version(1); v <= 40; v++ { + for l := L; l <= H; l++ { + for _, in := range input { + if !test(t, v, l, in) { + if badvers++; badvers >= 10 { + t.Errorf("stopping after %d bad versions", badvers) + break Version + } + } + } + } + } +} + +func TestEncode(t *testing.T) { + data := []byte{0x10, 0x20, 0x0c, 0x56, 0x61, 0x80, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11} + check := []byte{0xa5, 0x24, 0xd4, 0xc1, 0xed, 0x36, 0xc7, 0x87, 0x2c, 0x55} + rs := gf256.NewRSEncoder(Field, len(check)) + out := make([]byte, len(check)) + rs.ECC(data, out) + if !bytes.Equal(out, check) { + t.Errorf("have %x want %x", out, check) + } +} diff --git a/Godeps/_workspace/src/code.google.com/p/rsc/qr/libqrencode/qrencode.go b/Godeps/_workspace/src/code.google.com/p/rsc/qr/libqrencode/qrencode.go new file mode 100644 index 00000000000..f4ce3ffb666 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/rsc/qr/libqrencode/qrencode.go @@ -0,0 +1,149 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package libqrencode wraps the C libqrencode library. +// The qr package (in this package's parent directory) +// does not use any C wrapping. This code is here only +// for use during that package's tests. +package libqrencode + +/* +#cgo LDFLAGS: -lqrencode +#include +*/ +import "C" + +import ( + "fmt" + "image" + "image/color" + "unsafe" +) + +type Version int + +type Mode int + +const ( + Numeric Mode = C.QR_MODE_NUM + Alphanumeric Mode = C.QR_MODE_AN + EightBit Mode = C.QR_MODE_8 +) + +type Level int + +const ( + L Level = C.QR_ECLEVEL_L + M Level = C.QR_ECLEVEL_M + Q Level = C.QR_ECLEVEL_Q + H Level = C.QR_ECLEVEL_H +) + +type Pixel int + +const ( + Black Pixel = 1 << iota + DataECC + Format + PVersion + Timing + Alignment + Finder + NonData +) + +type Code struct { + Version int + Width int + Pixel [][]Pixel + Scale int +} + +func (*Code) ColorModel() color.Model { + return color.RGBAModel +} + +func (c *Code) Bounds() image.Rectangle { + d := (c.Width + 8) * c.Scale + return image.Rect(0, 0, d, d) +} + +var ( + white color.Color = color.RGBA{0xFF, 0xFF, 0xFF, 0xFF} + black color.Color = color.RGBA{0x00, 0x00, 0x00, 0xFF} + blue color.Color = color.RGBA{0x00, 0x00, 0x80, 0xFF} + red color.Color = color.RGBA{0xFF, 0x40, 0x40, 0xFF} + yellow color.Color = color.RGBA{0xFF, 0xFF, 0x00, 0xFF} + gray color.Color = color.RGBA{0x80, 0x80, 0x80, 0xFF} + green color.Color = color.RGBA{0x22, 0x8B, 0x22, 0xFF} +) + +func (c *Code) At(x, y int) color.Color { + x = x/c.Scale - 4 + y = y/c.Scale - 4 + if 0 <= x && x < c.Width && 0 <= y && y < c.Width { + switch p := c.Pixel[y][x]; { + case p&Black == 0: + // nothing + case p&DataECC != 0: + return black + case p&Format != 0: + return blue + case p&PVersion != 0: + return red + case p&Timing != 0: + return yellow + case p&Alignment != 0: + return gray + case p&Finder != 0: + return green + } + } + return white +} + +type Chunk struct { + Mode Mode + Text string +} + +func Encode(version Version, level Level, mode Mode, text string) (*Code, error) { + return EncodeChunk(version, level, Chunk{mode, text}) +} + +func EncodeChunk(version Version, level Level, chunk ...Chunk) (*Code, error) { + qi, err := C.QRinput_new2(C.int(version), C.QRecLevel(level)) + if qi == nil { + return nil, fmt.Errorf("QRinput_new2: %v", err) + } + defer C.QRinput_free(qi) + for _, ch := range chunk { + data := []byte(ch.Text) + n, err := C.QRinput_append(qi, C.QRencodeMode(ch.Mode), C.int(len(data)), (*C.uchar)(&data[0])) + if n < 0 { + return nil, fmt.Errorf("QRinput_append %q: %v", data, err) + } + } + + qc, err := C.QRcode_encodeInput(qi) + if qc == nil { + return nil, fmt.Errorf("QRinput_encodeInput: %v", err) + } + + c := &Code{ + Version: int(qc.version), + Width: int(qc.width), + Scale: 16, + } + pix := make([]Pixel, c.Width*c.Width) + cdat := (*[1000 * 1000]byte)(unsafe.Pointer(qc.data))[:len(pix)] + for i := range pix { + pix[i] = Pixel(cdat[i]) + } + c.Pixel = make([][]Pixel, c.Width) + for i := range c.Pixel { + c.Pixel[i] = pix[i*c.Width : (i+1)*c.Width] + } + return c, nil +} diff --git a/Godeps/_workspace/src/code.google.com/p/rsc/qr/png.go b/Godeps/_workspace/src/code.google.com/p/rsc/qr/png.go new file mode 100644 index 00000000000..db49d057726 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/rsc/qr/png.go @@ -0,0 +1,400 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package qr + +// PNG writer for QR codes. + +import ( + "bytes" + "encoding/binary" + "hash" + "hash/crc32" +) + +// PNG returns a PNG image displaying the code. +// +// PNG uses a custom encoder tailored to QR codes. +// Its compressed size is about 2x away from optimal, +// but it runs about 20x faster than calling png.Encode +// on c.Image(). +func (c *Code) PNG() []byte { + var p pngWriter + return p.encode(c) +} + +type pngWriter struct { + tmp [16]byte + wctmp [4]byte + buf bytes.Buffer + zlib bitWriter + crc hash.Hash32 +} + +var pngHeader = []byte("\x89PNG\r\n\x1a\n") + +func (w *pngWriter) encode(c *Code) []byte { + scale := c.Scale + siz := c.Size + + w.buf.Reset() + + // Header + w.buf.Write(pngHeader) + + // Header block + binary.BigEndian.PutUint32(w.tmp[0:4], uint32((siz+8)*scale)) + binary.BigEndian.PutUint32(w.tmp[4:8], uint32((siz+8)*scale)) + w.tmp[8] = 1 // 1-bit + w.tmp[9] = 0 // gray + w.tmp[10] = 0 + w.tmp[11] = 0 + w.tmp[12] = 0 + w.writeChunk("IHDR", w.tmp[:13]) + + // Comment + w.writeChunk("tEXt", comment) + + // Data + w.zlib.writeCode(c) + w.writeChunk("IDAT", w.zlib.bytes.Bytes()) + + // End + w.writeChunk("IEND", nil) + + return w.buf.Bytes() +} + +var comment = []byte("Software\x00QR-PNG http://qr.swtch.com/") + +func (w *pngWriter) writeChunk(name string, data []byte) { + if w.crc == nil { + w.crc = crc32.NewIEEE() + } + binary.BigEndian.PutUint32(w.wctmp[0:4], uint32(len(data))) + w.buf.Write(w.wctmp[0:4]) + w.crc.Reset() + copy(w.wctmp[0:4], name) + w.buf.Write(w.wctmp[0:4]) + w.crc.Write(w.wctmp[0:4]) + w.buf.Write(data) + w.crc.Write(data) + crc := w.crc.Sum32() + binary.BigEndian.PutUint32(w.wctmp[0:4], crc) + w.buf.Write(w.wctmp[0:4]) +} + +func (b *bitWriter) writeCode(c *Code) { + const ftNone = 0 + + b.adler32.Reset() + b.bytes.Reset() + b.nbit = 0 + + scale := c.Scale + siz := c.Size + + // zlib header + b.tmp[0] = 0x78 + b.tmp[1] = 0 + b.tmp[1] += uint8(31 - (uint16(b.tmp[0])<<8+uint16(b.tmp[1]))%31) + b.bytes.Write(b.tmp[0:2]) + + // Start flate block. + b.writeBits(1, 1, false) // final block + b.writeBits(1, 2, false) // compressed, fixed Huffman tables + + // White border. + // First row. + b.byte(ftNone) + n := (scale*(siz+8) + 7) / 8 + b.byte(255) + b.repeat(n-1, 1) + // 4*scale rows total. + b.repeat((4*scale-1)*(1+n), 1+n) + + for i := 0; i < 4*scale; i++ { + b.adler32.WriteNByte(ftNone, 1) + b.adler32.WriteNByte(255, n) + } + + row := make([]byte, 1+n) + for y := 0; y < siz; y++ { + row[0] = ftNone + j := 1 + var z uint8 + nz := 0 + for x := -4; x < siz+4; x++ { + // Raw data. + for i := 0; i < scale; i++ { + z <<= 1 + if !c.Black(x, y) { + z |= 1 + } + if nz++; nz == 8 { + row[j] = z + j++ + nz = 0 + } + } + } + if j < len(row) { + row[j] = z + } + for _, z := range row { + b.byte(z) + } + + // Scale-1 copies. + b.repeat((scale-1)*(1+n), 1+n) + + b.adler32.WriteN(row, scale) + } + + // White border. + // First row. + b.byte(ftNone) + b.byte(255) + b.repeat(n-1, 1) + // 4*scale rows total. + b.repeat((4*scale-1)*(1+n), 1+n) + + for i := 0; i < 4*scale; i++ { + b.adler32.WriteNByte(ftNone, 1) + b.adler32.WriteNByte(255, n) + } + + // End of block. + b.hcode(256) + b.flushBits() + + // adler32 + binary.BigEndian.PutUint32(b.tmp[0:], b.adler32.Sum32()) + b.bytes.Write(b.tmp[0:4]) +} + +// A bitWriter is a write buffer for bit-oriented data like deflate. +type bitWriter struct { + bytes bytes.Buffer + bit uint32 + nbit uint + + tmp [4]byte + adler32 adigest +} + +func (b *bitWriter) writeBits(bit uint32, nbit uint, rev bool) { + // reverse, for huffman codes + if rev { + br := uint32(0) + for i := uint(0); i < nbit; i++ { + br |= ((bit >> i) & 1) << (nbit - 1 - i) + } + bit = br + } + b.bit |= bit << b.nbit + b.nbit += nbit + for b.nbit >= 8 { + b.bytes.WriteByte(byte(b.bit)) + b.bit >>= 8 + b.nbit -= 8 + } +} + +func (b *bitWriter) flushBits() { + if b.nbit > 0 { + b.bytes.WriteByte(byte(b.bit)) + b.nbit = 0 + b.bit = 0 + } +} + +func (b *bitWriter) hcode(v int) { + /* + Lit Value Bits Codes + --------- ---- ----- + 0 - 143 8 00110000 through + 10111111 + 144 - 255 9 110010000 through + 111111111 + 256 - 279 7 0000000 through + 0010111 + 280 - 287 8 11000000 through + 11000111 + */ + switch { + case v <= 143: + b.writeBits(uint32(v)+0x30, 8, true) + case v <= 255: + b.writeBits(uint32(v-144)+0x190, 9, true) + case v <= 279: + b.writeBits(uint32(v-256)+0, 7, true) + case v <= 287: + b.writeBits(uint32(v-280)+0xc0, 8, true) + default: + panic("invalid hcode") + } +} + +func (b *bitWriter) byte(x byte) { + b.hcode(int(x)) +} + +func (b *bitWriter) codex(c int, val int, nx uint) { + b.hcode(c + val>>nx) + b.writeBits(uint32(val)&(1<= 258+3; n -= 258 { + b.repeat1(258, d) + } + if n > 258 { + // 258 < n < 258+3 + b.repeat1(10, d) + b.repeat1(n-10, d) + return + } + if n < 3 { + panic("invalid flate repeat") + } + b.repeat1(n, d) +} + +func (b *bitWriter) repeat1(n, d int) { + /* + Extra Extra Extra + Code Bits Length(s) Code Bits Lengths Code Bits Length(s) + ---- ---- ------ ---- ---- ------- ---- ---- ------- + 257 0 3 267 1 15,16 277 4 67-82 + 258 0 4 268 1 17,18 278 4 83-98 + 259 0 5 269 2 19-22 279 4 99-114 + 260 0 6 270 2 23-26 280 4 115-130 + 261 0 7 271 2 27-30 281 5 131-162 + 262 0 8 272 2 31-34 282 5 163-194 + 263 0 9 273 3 35-42 283 5 195-226 + 264 0 10 274 3 43-50 284 5 227-257 + 265 1 11,12 275 3 51-58 285 0 258 + 266 1 13,14 276 3 59-66 + */ + switch { + case n <= 10: + b.codex(257, n-3, 0) + case n <= 18: + b.codex(265, n-11, 1) + case n <= 34: + b.codex(269, n-19, 2) + case n <= 66: + b.codex(273, n-35, 3) + case n <= 130: + b.codex(277, n-67, 4) + case n <= 257: + b.codex(281, n-131, 5) + case n == 258: + b.hcode(285) + default: + panic("invalid repeat length") + } + + /* + Extra Extra Extra + Code Bits Dist Code Bits Dist Code Bits Distance + ---- ---- ---- ---- ---- ------ ---- ---- -------- + 0 0 1 10 4 33-48 20 9 1025-1536 + 1 0 2 11 4 49-64 21 9 1537-2048 + 2 0 3 12 5 65-96 22 10 2049-3072 + 3 0 4 13 5 97-128 23 10 3073-4096 + 4 1 5,6 14 6 129-192 24 11 4097-6144 + 5 1 7,8 15 6 193-256 25 11 6145-8192 + 6 2 9-12 16 7 257-384 26 12 8193-12288 + 7 2 13-16 17 7 385-512 27 12 12289-16384 + 8 3 17-24 18 8 513-768 28 13 16385-24576 + 9 3 25-32 19 8 769-1024 29 13 24577-32768 + */ + if d <= 4 { + b.writeBits(uint32(d-1), 5, true) + } else if d <= 32768 { + nbit := uint(16) + for d <= 1<<(nbit-1) { + nbit-- + } + v := uint32(d - 1) + v &^= 1 << (nbit - 1) // top bit is implicit + code := uint32(2*nbit - 2) // second bit is low bit of code + code |= v >> (nbit - 2) + v &^= 1 << (nbit - 2) + b.writeBits(code, 5, true) + // rest of bits follow + b.writeBits(uint32(v), nbit-2, false) + } else { + panic("invalid repeat distance") + } +} + +func (b *bitWriter) run(v byte, n int) { + if n == 0 { + return + } + b.byte(v) + if n-1 < 3 { + for i := 0; i < n-1; i++ { + b.byte(v) + } + } else { + b.repeat(n-1, 1) + } +} + +type adigest struct { + a, b uint32 +} + +func (d *adigest) Reset() { d.a, d.b = 1, 0 } + +const amod = 65521 + +func aupdate(a, b uint32, pi byte, n int) (aa, bb uint32) { + // TODO(rsc): 6g doesn't do magic multiplies for b %= amod, + // only for b = b%amod. + + // invariant: a, b < amod + if pi == 0 { + b += uint32(n%amod) * a + b = b % amod + return a, b + } + + // n times: + // a += pi + // b += a + // is same as + // b += n*a + n*(n+1)/2*pi + // a += n*pi + m := uint32(n) + b += (m % amod) * a + b = b % amod + b += (m * (m + 1) / 2) % amod * uint32(pi) + b = b % amod + a += (m % amod) * uint32(pi) + a = a % amod + return a, b +} + +func afinish(a, b uint32) uint32 { + return b<<16 | a +} + +func (d *adigest) WriteN(p []byte, n int) { + for i := 0; i < n; i++ { + for _, pi := range p { + d.a, d.b = aupdate(d.a, d.b, pi, 1) + } + } +} + +func (d *adigest) WriteNByte(pi byte, n int) { + d.a, d.b = aupdate(d.a, d.b, pi, n) +} + +func (d *adigest) Sum32() uint32 { return afinish(d.a, d.b) } diff --git a/Godeps/_workspace/src/code.google.com/p/rsc/qr/png_test.go b/Godeps/_workspace/src/code.google.com/p/rsc/qr/png_test.go new file mode 100644 index 00000000000..27a6229244a --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/rsc/qr/png_test.go @@ -0,0 +1,73 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package qr + +import ( + "bytes" + "image" + "image/color" + "image/png" + "io/ioutil" + "testing" +) + +func TestPNG(t *testing.T) { + c, err := Encode("hello, world", L) + if err != nil { + t.Fatal(err) + } + pngdat := c.PNG() + if true { + ioutil.WriteFile("x.png", pngdat, 0666) + } + m, err := png.Decode(bytes.NewBuffer(pngdat)) + if err != nil { + t.Fatal(err) + } + gm := m.(*image.Gray) + + scale := c.Scale + siz := c.Size + nbad := 0 + for y := 0; y < scale*(8+siz); y++ { + for x := 0; x < scale*(8+siz); x++ { + v := byte(255) + if c.Black(x/scale-4, y/scale-4) { + v = 0 + } + if gv := gm.At(x, y).(color.Gray).Y; gv != v { + t.Errorf("%d,%d = %d, want %d", x, y, gv, v) + if nbad++; nbad >= 20 { + t.Fatalf("too many bad pixels") + } + } + } + } +} + +func BenchmarkPNG(b *testing.B) { + c, err := Encode("0123456789012345678901234567890123456789", L) + if err != nil { + panic(err) + } + var bytes []byte + for i := 0; i < b.N; i++ { + bytes = c.PNG() + } + b.SetBytes(int64(len(bytes))) +} + +func BenchmarkImagePNG(b *testing.B) { + c, err := Encode("0123456789012345678901234567890123456789", L) + if err != nil { + panic(err) + } + var buf bytes.Buffer + for i := 0; i < b.N; i++ { + buf.Reset() + png.Encode(&buf, c.Image()) + } + b.SetBytes(int64(buf.Len())) +} diff --git a/Godeps/_workspace/src/code.google.com/p/rsc/qr/qr.go b/Godeps/_workspace/src/code.google.com/p/rsc/qr/qr.go new file mode 100644 index 00000000000..d45256f00ac --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/rsc/qr/qr.go @@ -0,0 +1,116 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +/* +Package qr encodes QR codes. +*/ +package qr + +import ( + "errors" + "image" + "image/color" + + "github.com/gravitational/teleport/Godeps/_workspace/src/code.google.com/p/rsc/qr/coding" +) + +// A Level denotes a QR error correction level. +// From least to most tolerant of errors, they are L, M, Q, H. +type Level int + +const ( + L Level = iota // 20% redundant + M // 38% redundant + Q // 55% redundant + H // 65% redundant +) + +// Encode returns an encoding of text at the given error correction level. +func Encode(text string, level Level) (*Code, error) { + // Pick data encoding, smallest first. + // We could split the string and use different encodings + // but that seems like overkill for now. + var enc coding.Encoding + switch { + case coding.Num(text).Check() == nil: + enc = coding.Num(text) + case coding.Alpha(text).Check() == nil: + enc = coding.Alpha(text) + default: + enc = coding.String(text) + } + + // Pick size. + l := coding.Level(level) + var v coding.Version + for v = coding.MinVersion; ; v++ { + if v > coding.MaxVersion { + return nil, errors.New("text too long to encode as QR") + } + if enc.Bits(v) <= v.DataBytes(l)*8 { + break + } + } + + // Build and execute plan. + p, err := coding.NewPlan(v, l, 0) + if err != nil { + return nil, err + } + cc, err := p.Encode(enc) + if err != nil { + return nil, err + } + + // TODO: Pick appropriate mask. + + return &Code{cc.Bitmap, cc.Size, cc.Stride, 8}, nil +} + +// A Code is a square pixel grid. +// It implements image.Image and direct PNG encoding. +type Code struct { + Bitmap []byte // 1 is black, 0 is white + Size int // number of pixels on a side + Stride int // number of bytes per row + Scale int // number of image pixels per QR pixel +} + +// Black returns true if the pixel at (x,y) is black. +func (c *Code) Black(x, y int) bool { + return 0 <= x && x < c.Size && 0 <= y && y < c.Size && + c.Bitmap[y*c.Stride+x/8]&(1<> 24), byte(rgba >> 16), byte(rgba >> 8), byte(rgba)} + draw.Draw(c, r, u, image.ZP, draw.Src) + } + } + + if csize != 0 { + if font == "" { + font = "data/luxisr.ttf" + } + ctxt := fs.NewContext(req) + dat, _, err := ctxt.Read(font) + if err != nil { + panic(err) + } + tfont, err := freetype.ParseFont(dat) + if err != nil { + panic(err) + } + ft := freetype.NewContext() + ft.SetDst(c) + ft.SetDPI(100) + ft.SetFont(tfont) + ft.SetFontSize(float64(pt)) + ft.SetSrc(image.NewUniform(color.Black)) + ft.SetClip(image.Rect(0, 0, 0, 0)) + wid, err := ft.DrawString(caption, freetype.Pt(0, 0)) + if err != nil { + panic(err) + } + p := freetype.Pt(d, d+3*pt/2) + p.X -= wid.X + p.X /= 2 + ft.SetClip(c.Bounds()) + ft.DrawString(caption, p) + } + + return c +} + +func makeFrame(req *http.Request, font string, pt, vers, l, scale, dots int) image.Image { + lev := coding.Level(l) + p, err := coding.NewPlan(coding.Version(vers), lev, 0) + if err != nil { + panic(err) + } + + nd := p.DataBytes / p.Blocks + nc := p.CheckBytes / p.Blocks + extra := p.DataBytes - nd*p.Blocks + + cap := fmt.Sprintf("QR v%d, %s", vers, lev) + if dots > 0 { + cap = fmt.Sprintf("QR v%d order, from bottom right", vers) + } + m := makeImage(req, cap, font, pt, len(p.Pixel), 0, scale, func(x, y int) uint32 { + pix := p.Pixel[y][x] + switch pix.Role() { + case coding.Data: + if dots > 0 { + return 0xffffffff + } + off := int(pix.Offset() / 8) + nd := nd + var i int + for i = 0; i < p.Blocks; i++ { + if i == extra { + nd++ + } + if off < nd { + break + } + off -= nd + } + return blockColors[i%len(blockColors)] + case coding.Check: + if dots > 0 { + return 0xffffffff + } + i := (int(pix.Offset()/8) - p.DataBytes) / nc + return dark(blockColors[i%len(blockColors)]) + } + if pix&coding.Black != 0 { + return 0x000000ff + } + return 0xffffffff + }) + + if dots > 0 { + b := m.Bounds() + for y := 0; y <= len(p.Pixel); y++ { + for x := 0; x < b.Dx(); x++ { + m.SetRGBA(x, y*scale-(y/len(p.Pixel)), color.RGBA{127, 127, 127, 255}) + } + } + for x := 0; x <= len(p.Pixel); x++ { + for y := 0; y < b.Dx(); y++ { + m.SetRGBA(x*scale-(x/len(p.Pixel)), y, color.RGBA{127, 127, 127, 255}) + } + } + order := make([]image.Point, (p.DataBytes+p.CheckBytes)*8+1) + for y, row := range p.Pixel { + for x, pix := range row { + if r := pix.Role(); r != coding.Data && r != coding.Check { + continue + } + // draw.Draw(m, m.Bounds().Add(image.Pt(x*scale, y*scale)), dot, image.ZP, draw.Over) + order[pix.Offset()] = image.Point{x*scale + scale/2, y*scale + scale/2} + } + } + + for mode := 0; mode < 2; mode++ { + for i, p := range order { + q := order[i+1] + if q.X == 0 { + break + } + line(m, p, q, mode) + } + } + } + return m +} + +func line(m *image.RGBA, p, q image.Point, mode int) { + x := 0 + y := 0 + dx := q.X - p.X + dy := q.Y - p.Y + xsign := +1 + ysign := +1 + if dx < 0 { + xsign = -1 + dx = -dx + } + if dy < 0 { + ysign = -1 + dy = -dy + } + pt := func() { + switch mode { + case 0: + for dx := -2; dx <= 2; dx++ { + for dy := -2; dy <= 2; dy++ { + if dy*dx <= -4 || dy*dx >= 4 { + continue + } + m.SetRGBA(p.X+x*xsign+dx, p.Y+y*ysign+dy, color.RGBA{255, 192, 192, 255}) + } + } + + case 1: + m.SetRGBA(p.X+x*xsign, p.Y+y*ysign, color.RGBA{128, 0, 0, 255}) + } + } + if dx > dy { + for x < dx || y < dy { + pt() + x++ + if float64(x)*float64(dy)/float64(dx)-float64(y) > 0.5 { + y++ + } + } + } else { + for x < dx || y < dy { + pt() + y++ + if float64(y)*float64(dx)/float64(dy)-float64(x) > 0.5 { + x++ + } + } + } + pt() +} + +func pngEncode(c image.Image) []byte { + var b bytes.Buffer + png.Encode(&b, c) + return b.Bytes() +} + +// Frame handles a request for a single QR frame. +func Frame(w http.ResponseWriter, req *http.Request) { + arg := func(s string) int { x, _ := strconv.Atoi(req.FormValue(s)); return x } + v := arg("v") + scale := arg("scale") + if scale == 0 { + scale = 8 + } + + w.Header().Set("Cache-Control", "public, max-age=3600") + w.Write(pngEncode(makeFrame(req, req.FormValue("font"), arg("pt"), v, arg("l"), scale, arg("dots")))) +} + +// Frames handles a request for multiple QR frames. +func Frames(w http.ResponseWriter, req *http.Request) { + vs := strings.Split(req.FormValue("v"), ",") + + arg := func(s string) int { x, _ := strconv.Atoi(req.FormValue(s)); return x } + scale := arg("scale") + if scale == 0 { + scale = 8 + } + font := req.FormValue("font") + pt := arg("pt") + dots := arg("dots") + + var images []image.Image + l := arg("l") + for _, v := range vs { + l := l + if i := strings.Index(v, "."); i >= 0 { + l, _ = strconv.Atoi(v[i+1:]) + v = v[:i] + } + vv, _ := strconv.Atoi(v) + images = append(images, makeFrame(req, font, pt, vv, l, scale, dots)) + } + + b := images[len(images)-1].Bounds() + + dx := arg("dx") + if dx == 0 { + dx = b.Dx() + } + x, y := 0, 0 + xmax := 0 + sep := arg("sep") + if sep == 0 { + sep = 10 + } + var points []image.Point + for i, m := range images { + if x > 0 { + x += sep + } + if x > 0 && x+m.Bounds().Dx() > dx { + y += sep + images[i-1].Bounds().Dy() + x = 0 + } + points = append(points, image.Point{x, y}) + x += m.Bounds().Dx() + if x > xmax { + xmax = x + } + + } + + c := image.NewRGBA(image.Rect(0, 0, xmax, y+b.Dy())) + for i, m := range images { + draw.Draw(c, c.Bounds().Add(points[i]), m, image.ZP, draw.Src) + } + + w.Header().Set("Cache-Control", "public, max-age=3600") + w.Write(pngEncode(c)) +} + +// Mask handles a request for a single QR mask. +func Mask(w http.ResponseWriter, req *http.Request) { + arg := func(s string) int { x, _ := strconv.Atoi(req.FormValue(s)); return x } + v := arg("v") + m := arg("m") + scale := arg("scale") + if scale == 0 { + scale = 8 + } + + w.Header().Set("Cache-Control", "public, max-age=3600") + w.Write(pngEncode(makeMask(req, req.FormValue("font"), arg("pt"), v, m, scale))) +} + +// Masks handles a request for multiple QR masks. +func Masks(w http.ResponseWriter, req *http.Request) { + arg := func(s string) int { x, _ := strconv.Atoi(req.FormValue(s)); return x } + v := arg("v") + scale := arg("scale") + if scale == 0 { + scale = 8 + } + font := req.FormValue("font") + pt := arg("pt") + var mm []image.Image + for m := 0; m < 8; m++ { + mm = append(mm, makeMask(req, font, pt, v, m, scale)) + } + dx := mm[0].Bounds().Dx() + dy := mm[0].Bounds().Dy() + + sep := arg("sep") + if sep == 0 { + sep = 10 + } + c := image.NewRGBA(image.Rect(0, 0, (dx+sep)*4-sep, (dy+sep)*2-sep)) + for m := 0; m < 8; m++ { + x := (m % 4) * (dx + sep) + y := (m / 4) * (dy + sep) + draw.Draw(c, c.Bounds().Add(image.Pt(x, y)), mm[m], image.ZP, draw.Src) + } + + w.Header().Set("Cache-Control", "public, max-age=3600") + w.Write(pngEncode(c)) +} + +var maskName = []string{ + "(x+y) % 2", + "y % 2", + "x % 3", + "(x+y) % 3", + "(y/2 + x/3) % 2", + "xy%2 + xy%3", + "(xy%2 + xy%3) % 2", + "(xy%3 + (x+y)%2) % 2", +} + +func makeMask(req *http.Request, font string, pt int, vers, mask, scale int) image.Image { + p, err := coding.NewPlan(coding.Version(vers), coding.L, coding.Mask(mask)) + if err != nil { + panic(err) + } + m := makeImage(req, maskName[mask], font, pt, len(p.Pixel), 0, scale, func(x, y int) uint32 { + pix := p.Pixel[y][x] + switch pix.Role() { + case coding.Data, coding.Check: + if pix&coding.Invert != 0 { + return 0x000000ff + } + } + return 0xffffffff + }) + return m +} + +var blockColors = []uint32{ + 0x7777ffff, + 0xffff77ff, + 0xff7777ff, + 0x77ffffff, + 0x1e90ffff, + 0xffffe0ff, + 0x8b6969ff, + 0x77ff77ff, + 0x9b30ffff, + 0x00bfffff, + 0x90e890ff, + 0xfff68fff, + 0xffec8bff, + 0xffa07aff, + 0xffa54fff, + 0xeee8aaff, + 0x98fb98ff, + 0xbfbfbfff, + 0x54ff9fff, + 0xffaeb9ff, + 0xb23aeeff, + 0xbbffffff, + 0x7fffd4ff, + 0xff7a7aff, + 0x00007fff, +} + +func dark(x uint32) uint32 { + r, g, b, a := byte(x>>24), byte(x>>16), byte(x>>8), byte(x) + r = r/2 + r/4 + g = g/2 + g/4 + b = b/2 + b/4 + return uint32(r)<<24 | uint32(g)<<16 | uint32(b)<<8 | uint32(a) +} + +func clamp(x int) byte { + if x < 0 { + return 0 + } + if x > 255 { + return 255 + } + return byte(x) +} + +func max(x, y int) int { + if x > y { + return x + } + return y +} + +// Arrow handles a request for an arrow pointing in a given direction. +func Arrow(w http.ResponseWriter, req *http.Request) { + arg := func(s string) int { x, _ := strconv.Atoi(req.FormValue(s)); return x } + dir := arg("dir") + size := arg("size") + if size == 0 { + size = 50 + } + del := size / 10 + + m := image.NewRGBA(image.Rect(0, 0, size, size)) + + if dir == 4 { + draw.Draw(m, m.Bounds(), image.Black, image.ZP, draw.Src) + draw.Draw(m, image.Rect(5, 5, size-5, size-5), image.White, image.ZP, draw.Src) + } + + pt := func(x, y int, c color.RGBA) { + switch dir { + case 0: + m.SetRGBA(x, y, c) + case 1: + m.SetRGBA(y, size-1-x, c) + case 2: + m.SetRGBA(size-1-x, size-1-y, c) + case 3: + m.SetRGBA(size-1-y, x, c) + } + } + + for y := 0; y < size/2; y++ { + for x := 0; x < del && x < y; x++ { + pt(x, y, color.RGBA{0, 0, 0, 255}) + } + for x := del; x < y-del; x++ { + pt(x, y, color.RGBA{128, 128, 255, 255}) + } + for x := max(y-del, 0); x <= y; x++ { + pt(x, y, color.RGBA{0, 0, 0, 255}) + } + } + for y := size / 2; y < size; y++ { + for x := 0; x < del && x < size-1-y; x++ { + pt(x, y, color.RGBA{0, 0, 0, 255}) + } + for x := del; x < size-1-y-del; x++ { + pt(x, y, color.RGBA{128, 128, 192, 255}) + } + for x := max(size-1-y-del, 0); x <= size-1-y; x++ { + pt(x, y, color.RGBA{0, 0, 0, 255}) + } + } + + w.Header().Set("Cache-Control", "public, max-age=3600") + w.Write(pngEncode(m)) +} + +// Encode encodes a string using the given version, level, and mask. +func Encode(w http.ResponseWriter, req *http.Request) { + val := func(s string) int { + v, _ := strconv.Atoi(req.FormValue(s)) + return v + } + + l := coding.Level(val("l")) + v := coding.Version(val("v")) + enc := coding.String(req.FormValue("t")) + m := coding.Mask(val("m")) + + p, err := coding.NewPlan(v, l, m) + if err != nil { + panic(err) + } + cc, err := p.Encode(enc) + if err != nil { + panic(err) + } + + c := &qr.Code{Bitmap: cc.Bitmap, Size: cc.Size, Stride: cc.Stride, Scale: 8} + w.Header().Set("Content-Type", "image/png") + w.Header().Set("Cache-Control", "public, max-age=3600") + w.Write(c.PNG()) +} diff --git a/Godeps/_workspace/src/code.google.com/p/rsc/qr/web/play.go b/Godeps/_workspace/src/code.google.com/p/rsc/qr/web/play.go new file mode 100644 index 00000000000..6e795f81ae8 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/rsc/qr/web/play.go @@ -0,0 +1,1118 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +/* +QR data layout + +qr/ + upload/ + id.png + id.fix + flag/ + id + +*/ +// TODO: Random seed taken from GET for caching, repeatability. +// TODO: Flag for abuse button + some kind of dashboard. +// TODO: +1 button on web page? permalink? +// TODO: Flag for abuse button on permalinks too? +// TODO: Make the page prettier. +// TODO: Cache headers. + +package web + +import ( + "bytes" + "crypto/md5" + "encoding/base64" + "encoding/json" + "fmt" + "html/template" + "image" + "image/color" + _ "image/gif" + _ "image/jpeg" + "image/png" + "io" + "math/rand" + "net/http" + "net/url" + "os" + "sort" + "strconv" + "strings" + "time" + + "code.google.com/p/rsc/appfs/fs" + "github.com/gravitational/teleport/Godeps/_workspace/src/code.google.com/p/rsc/gf256" + "github.com/gravitational/teleport/Godeps/_workspace/src/code.google.com/p/rsc/qr" + "github.com/gravitational/teleport/Godeps/_workspace/src/code.google.com/p/rsc/qr/coding" + "github.com/gravitational/teleport/Godeps/_workspace/src/code.google.com/p/rsc/qr/web/resize" +) + +func runTemplate(c *fs.Context, w http.ResponseWriter, name string, data interface{}) { + t := template.New("main") + + main, _, err := c.Read(name) + if err != nil { + panic(err) + } + style, _, _ := c.Read("style.html") + main = append(main, style...) + _, err = t.Parse(string(main)) + if err != nil { + panic(err) + } + + var buf bytes.Buffer + if err := t.Execute(&buf, &data); err != nil { + panic(err) + } + w.Write(buf.Bytes()) +} + +func isImgName(s string) bool { + if len(s) != 32 { + return false + } + for i := 0; i < len(s); i++ { + if '0' <= s[i] && s[i] <= '9' || 'a' <= s[i] && s[i] <= 'f' { + continue + } + return false + } + return true +} + +func isTagName(s string) bool { + if len(s) != 16 { + return false + } + for i := 0; i < len(s); i++ { + if '0' <= s[i] && s[i] <= '9' || 'a' <= s[i] && s[i] <= 'f' { + continue + } + return false + } + return true +} + +// Draw is the handler for drawing a QR code. +func Draw(w http.ResponseWriter, req *http.Request) { + ctxt := fs.NewContext(req) + + url := req.FormValue("url") + if url == "" { + url = "http://swtch.com/qr" + } + if req.FormValue("upload") == "1" { + upload(w, req, url) + return + } + + t0 := time.Now() + img := req.FormValue("i") + if !isImgName(img) { + img = "pjw" + } + if req.FormValue("show") == "png" { + i := loadSize(ctxt, img, 48) + var buf bytes.Buffer + png.Encode(&buf, i) + w.Write(buf.Bytes()) + return + } + if req.FormValue("flag") == "1" { + flag(w, req, img, ctxt) + return + } + if req.FormValue("x") == "" { + var data = struct { + Name string + URL string + }{ + Name: img, + URL: url, + } + runTemplate(ctxt, w, "qr/main.html", &data) + return + } + + arg := func(s string) int { x, _ := strconv.Atoi(req.FormValue(s)); return x } + targ := makeTarg(ctxt, img, 17+4*arg("v")+arg("z")) + + m := &Image{ + Name: img, + Dx: arg("x"), + Dy: arg("y"), + URL: req.FormValue("u"), + Version: arg("v"), + Mask: arg("m"), + RandControl: arg("r") > 0, + Dither: arg("i") > 0, + OnlyDataBits: arg("d") > 0, + SaveControl: arg("c") > 0, + Scale: arg("scale"), + Target: targ, + Seed: int64(arg("s")), + Rotation: arg("o"), + Size: arg("z"), + } + if m.Version > 8 { + m.Version = 8 + } + + if m.Scale == 0 { + if arg("l") > 1 { + m.Scale = 8 + } else { + m.Scale = 4 + } + } + if m.Version >= 12 && m.Scale >= 4 { + m.Scale /= 2 + } + + if arg("l") == 1 { + data, err := json.Marshal(m) + if err != nil { + panic(err) + } + h := md5.New() + h.Write(data) + tag := fmt.Sprintf("%x", h.Sum(nil))[:16] + if err := ctxt.Write("qrsave/"+tag, data); err != nil { + panic(err) + } + http.Redirect(w, req, "/qr/show/"+tag, http.StatusTemporaryRedirect) + return + } + + if err := m.Encode(req); err != nil { + fmt.Fprintf(w, "%s\n", err) + return + } + + var dat []byte + switch { + case m.SaveControl: + dat = m.Control + default: + dat = m.Code.PNG() + } + + if arg("l") > 0 { + w.Header().Set("Content-Type", "image/png") + w.Write(dat) + return + } + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + fmt.Fprint(w, "

") + fmt.Fprintf(w, "
\n", m.Link()) + fmt.Fprintf(w, "
\n") + fmt.Fprintf(w, "
%v
\n", time.Now().Sub(t0)) +} + +func (m *Image) Small() bool { + return 8*(17+4*int(m.Version)) < 512 +} + +func (m *Image) Link() string { + s := fmt.Sprint + b := func(v bool) string { + if v { + return "1" + } + return "0" + } + val := url.Values{ + "i": {m.Name}, + "x": {s(m.Dx)}, + "y": {s(m.Dy)}, + "z": {s(m.Size)}, + "u": {m.URL}, + "v": {s(m.Version)}, + "m": {s(m.Mask)}, + "r": {b(m.RandControl)}, + "t": {b(m.Dither)}, + "d": {b(m.OnlyDataBits)}, + "c": {b(m.SaveControl)}, + "s": {s(m.Seed)}, + } + return "/qr/draw?" + val.Encode() +} + +// Show is the handler for showing a stored QR code. +func Show(w http.ResponseWriter, req *http.Request) { + ctxt := fs.NewContext(req) + tag := req.URL.Path[len("/qr/show/"):] + png := strings.HasSuffix(tag, ".png") + if png { + tag = tag[:len(tag)-len(".png")] + } + if !isTagName(tag) { + fmt.Fprintf(w, "Sorry, QR code not found\n") + return + } + if req.FormValue("flag") == "1" { + flag(w, req, tag, ctxt) + return + } + data, _, err := ctxt.Read("qrsave/" + tag) + if err != nil { + fmt.Fprintf(w, "Sorry, QR code not found.\n") + return + } + + var m Image + if err := json.Unmarshal(data, &m); err != nil { + panic(err) + } + m.Tag = tag + + switch req.FormValue("size") { + case "big": + m.Scale *= 2 + case "small": + m.Scale /= 2 + } + + if png { + if err := m.Encode(req); err != nil { + panic(err) + return + } + w.Header().Set("Cache-Control", "public, max-age=3600") + w.Write(m.Code.PNG()) + return + } + + w.Header().Set("Cache-Control", "public, max-age=300") + runTemplate(ctxt, w, "qr/permalink.html", &m) +} + +func upload(w http.ResponseWriter, req *http.Request, link string) { + // Upload of a new image. + // Copied from Moustachio demo. + f, _, err := req.FormFile("image") + if err != nil { + fmt.Fprintf(w, "You need to select an image to upload.\n") + return + } + defer f.Close() + + i, _, err := image.Decode(f) + if err != nil { + panic(err) + } + + // Convert image to 128x128 gray+alpha. + b := i.Bounds() + const max = 128 + // If it's gigantic, it's more efficient to downsample first + // and then resize; resizing will smooth out the roughness. + var i1 *image.RGBA + if b.Dx() > 4*max || b.Dy() > 4*max { + w, h := 2*max, 2*max + if b.Dx() > b.Dy() { + h = b.Dy() * h / b.Dx() + } else { + w = b.Dx() * w / b.Dy() + } + i1 = resize.Resample(i, b, w, h) + } else { + // "Resample" to same size, just to convert to RGBA. + i1 = resize.Resample(i, b, b.Dx(), b.Dy()) + } + b = i1.Bounds() + + // Encode to PNG. + dx, dy := 128, 128 + if b.Dx() > b.Dy() { + dy = b.Dy() * dx / b.Dx() + } else { + dx = b.Dx() * dy / b.Dy() + } + i128 := resize.ResizeRGBA(i1, i1.Bounds(), dx, dy) + + var buf bytes.Buffer + if err := png.Encode(&buf, i128); err != nil { + panic(err) + } + + h := md5.New() + h.Write(buf.Bytes()) + tag := fmt.Sprintf("%x", h.Sum(nil))[:32] + + ctxt := fs.NewContext(req) + if err := ctxt.Write("qr/upload/"+tag+".png", buf.Bytes()); err != nil { + panic(err) + } + + // Redirect with new image tag. + // Redirect to draw with new image tag. + http.Redirect(w, req, req.URL.Path+"?"+url.Values{"i": {tag}, "url": {link}}.Encode(), 302) +} + +func flag(w http.ResponseWriter, req *http.Request, img string, ctxt *fs.Context) { + if !isImgName(img) && !isTagName(img) { + fmt.Fprintf(w, "Invalid image.\n") + return + } + data, _, _ := ctxt.Read("qr/flag/" + img) + data = append(data, '!') + ctxt.Write("qr/flag/"+img, data) + + fmt.Fprintf(w, "Thank you. The image has been reported.\n") +} + +func loadSize(ctxt *fs.Context, name string, max int) *image.RGBA { + data, _, err := ctxt.Read("qr/upload/" + name + ".png") + if err != nil { + panic(err) + } + i, _, err := image.Decode(bytes.NewBuffer(data)) + if err != nil { + panic(err) + } + b := i.Bounds() + dx, dy := max, max + if b.Dx() > b.Dy() { + dy = b.Dy() * dx / b.Dx() + } else { + dx = b.Dx() * dy / b.Dy() + } + var irgba *image.RGBA + switch i := i.(type) { + case *image.RGBA: + irgba = resize.ResizeRGBA(i, i.Bounds(), dx, dy) + case *image.NRGBA: + irgba = resize.ResizeNRGBA(i, i.Bounds(), dx, dy) + } + return irgba +} + +func makeTarg(ctxt *fs.Context, name string, max int) [][]int { + i := loadSize(ctxt, name, max) + b := i.Bounds() + dx, dy := b.Dx(), b.Dy() + targ := make([][]int, dy) + arr := make([]int, dx*dy) + for y := 0; y < dy; y++ { + targ[y], arr = arr[:dx], arr[dx:] + row := targ[y] + for x := 0; x < dx; x++ { + p := i.Pix[y*i.Stride+4*x:] + r, g, b, a := p[0], p[1], p[2], p[3] + if a == 0 { + row[x] = -1 + } else { + row[x] = int((299*uint32(r) + 587*uint32(g) + 114*uint32(b) + 500) / 1000) + } + } + } + return targ +} + +type Image struct { + Name string + Target [][]int + Dx int + Dy int + URL string + Tag string + Version int + Mask int + Scale int + Rotation int + Size int + + // RandControl says to pick the pixels randomly. + RandControl bool + Seed int64 + + // Dither says to dither instead of using threshold pixel layout. + Dither bool + + // OnlyDataBits says to use only data bits, not check bits. + OnlyDataBits bool + + // Code is the final QR code. + Code *qr.Code + + // Control is a PNG showing the pixels that we controlled. + // Pixels we don't control are grayed out. + SaveControl bool + Control []byte +} + +type Pixinfo struct { + X int + Y int + Pix coding.Pixel + Targ byte + DTarg int + Contrast int + HardZero bool + Block *BitBlock + Bit uint +} + +type Pixorder struct { + Off int + Priority int +} + +type byPriority []Pixorder + +func (x byPriority) Len() int { return len(x) } +func (x byPriority) Swap(i, j int) { x[i], x[j] = x[j], x[i] } +func (x byPriority) Less(i, j int) bool { return x[i].Priority > x[j].Priority } + +func (m *Image) target(x, y int) (targ byte, contrast int) { + tx := x + m.Dx + ty := y + m.Dy + if ty < 0 || ty >= len(m.Target) || tx < 0 || tx >= len(m.Target[ty]) { + return 255, -1 + } + + v0 := m.Target[ty][tx] + if v0 < 0 { + return 255, -1 + } + targ = byte(v0) + + n := 0 + sum := 0 + sumsq := 0 + const del = 5 + for dy := -del; dy <= del; dy++ { + for dx := -del; dx <= del; dx++ { + if 0 <= ty+dy && ty+dy < len(m.Target) && 0 <= tx+dx && tx+dx < len(m.Target[ty+dy]) { + v := m.Target[ty+dy][tx+dx] + sum += v + sumsq += v * v + n++ + } + } + } + + avg := sum / n + contrast = sumsq/n - avg*avg + return +} + +func (m *Image) rotate(p *coding.Plan, rot int) { + if rot == 0 { + return + } + + N := len(p.Pixel) + pix := make([][]coding.Pixel, N) + apix := make([]coding.Pixel, N*N) + for i := range pix { + pix[i], apix = apix[:N], apix[N:] + } + + switch rot { + case 0: + // ok + case 1: + for y := 0; y < N; y++ { + for x := 0; x < N; x++ { + pix[y][x] = p.Pixel[x][N-1-y] + } + } + case 2: + for y := 0; y < N; y++ { + for x := 0; x < N; x++ { + pix[y][x] = p.Pixel[N-1-y][N-1-x] + } + } + case 3: + for y := 0; y < N; y++ { + for x := 0; x < N; x++ { + pix[y][x] = p.Pixel[N-1-x][y] + } + } + } + + p.Pixel = pix +} + +func (m *Image) Encode(req *http.Request) error { + p, err := coding.NewPlan(coding.Version(m.Version), coding.L, coding.Mask(m.Mask)) + if err != nil { + return err + } + + m.rotate(p, m.Rotation) + + rand := rand.New(rand.NewSource(m.Seed)) + + // QR parameters. + nd := p.DataBytes / p.Blocks + nc := p.CheckBytes / p.Blocks + extra := p.DataBytes - nd*p.Blocks + rs := gf256.NewRSEncoder(coding.Field, nc) + + // Build information about pixels, indexed by data/check bit number. + pixByOff := make([]Pixinfo, (p.DataBytes+p.CheckBytes)*8) + expect := make([][]bool, len(p.Pixel)) + for y, row := range p.Pixel { + expect[y] = make([]bool, len(row)) + for x, pix := range row { + targ, contrast := m.target(x, y) + if m.RandControl && contrast >= 0 { + contrast = rand.Intn(128) + 64*((x+y)%2) + 64*((x+y)%3%2) + } + expect[y][x] = pix&coding.Black != 0 + if r := pix.Role(); r == coding.Data || r == coding.Check { + pixByOff[pix.Offset()] = Pixinfo{X: x, Y: y, Pix: pix, Targ: targ, Contrast: contrast} + } + } + } + +Again: + // Count fixed initial data bits, prepare template URL. + url := m.URL + "#" + var b coding.Bits + coding.String(url).Encode(&b, p.Version) + coding.Num("").Encode(&b, p.Version) + bbit := b.Bits() + dbit := p.DataBytes*8 - bbit + if dbit < 0 { + return fmt.Errorf("cannot encode URL into available bits") + } + num := make([]byte, dbit/10*3) + for i := range num { + num[i] = '0' + } + b.Pad(dbit) + b.Reset() + coding.String(url).Encode(&b, p.Version) + coding.Num(num).Encode(&b, p.Version) + b.AddCheckBytes(p.Version, p.Level) + data := b.Bytes() + + doff := 0 // data offset + coff := 0 // checksum offset + mbit := bbit + dbit/10*10 + + // Choose pixels. + bitblocks := make([]*BitBlock, p.Blocks) + for blocknum := 0; blocknum < p.Blocks; blocknum++ { + if blocknum == p.Blocks-extra { + nd++ + } + + bdata := data[doff/8 : doff/8+nd] + cdata := data[p.DataBytes+coff/8 : p.DataBytes+coff/8+nc] + bb := newBlock(nd, nc, rs, bdata, cdata) + bitblocks[blocknum] = bb + + // Determine which bits in this block we can try to edit. + lo, hi := 0, nd*8 + if lo < bbit-doff { + lo = bbit - doff + if lo > hi { + lo = hi + } + } + if hi > mbit-doff { + hi = mbit - doff + if hi < lo { + hi = lo + } + } + + // Preserve [0, lo) and [hi, nd*8). + for i := 0; i < lo; i++ { + if !bb.canSet(uint(i), (bdata[i/8]>>uint(7-i&7))&1) { + return fmt.Errorf("cannot preserve required bits") + } + } + for i := hi; i < nd*8; i++ { + if !bb.canSet(uint(i), (bdata[i/8]>>uint(7-i&7))&1) { + return fmt.Errorf("cannot preserve required bits") + } + } + + // Can edit [lo, hi) and checksum bits to hit target. + // Determine which ones to try first. + order := make([]Pixorder, (hi-lo)+nc*8) + for i := lo; i < hi; i++ { + order[i-lo].Off = doff + i + } + for i := 0; i < nc*8; i++ { + order[hi-lo+i].Off = p.DataBytes*8 + coff + i + } + if m.OnlyDataBits { + order = order[:hi-lo] + } + for i := range order { + po := &order[i] + po.Priority = pixByOff[po.Off].Contrast<<8 | rand.Intn(256) + } + sort.Sort(byPriority(order)) + + const mark = false + for i := range order { + po := &order[i] + pinfo := &pixByOff[po.Off] + bval := pinfo.Targ + if bval < 128 { + bval = 1 + } else { + bval = 0 + } + pix := pinfo.Pix + if pix&coding.Invert != 0 { + bval ^= 1 + } + if pinfo.HardZero { + bval = 0 + } + + var bi int + if pix.Role() == coding.Data { + bi = po.Off - doff + } else { + bi = po.Off - p.DataBytes*8 - coff + nd*8 + } + if bb.canSet(uint(bi), bval) { + pinfo.Block = bb + pinfo.Bit = uint(bi) + if mark { + p.Pixel[pinfo.Y][pinfo.X] = coding.Black + } + } else { + if pinfo.HardZero { + panic("hard zero") + } + if mark { + p.Pixel[pinfo.Y][pinfo.X] = 0 + } + } + } + bb.copyOut() + + const cheat = false + for i := 0; i < nd*8; i++ { + pinfo := &pixByOff[doff+i] + pix := p.Pixel[pinfo.Y][pinfo.X] + if bb.B[i/8]&(1<= 128 { + // want white + pval = 0 + v = 255 + } + + bval := pval // bit value + if pix&coding.Invert != 0 { + bval ^= 1 + } + if pinfo.HardZero && bval != 0 { + bval ^= 1 + pval ^= 1 + v ^= 255 + } + + // Set pixel value as we want it. + pinfo.Block.reset(pinfo.Bit, bval) + + _, _ = x, y + + err := targ - v + if x+1 < len(row) { + addDither(pixByOff, row[x+1], err*7/16) + } + if false && y+1 < len(p.Pixel) { + if x > 0 { + addDither(pixByOff, p.Pixel[y+1][x-1], err*3/16) + } + addDither(pixByOff, p.Pixel[y+1][x], err*5/16) + if x+1 < len(row) { + addDither(pixByOff, p.Pixel[y+1][x+1], err*1/16) + } + } + } + } + + for _, bb := range bitblocks { + bb.copyOut() + } + } + + noops := 0 + // Copy numbers back out. + for i := 0; i < dbit/10; i++ { + // Pull out 10 bits. + v := 0 + for j := 0; j < 10; j++ { + bi := uint(bbit + 10*i + j) + v <<= 1 + v |= int((data[bi/8] >> (7 - bi&7)) & 1) + } + // Turn into 3 digits. + if v >= 1000 { + // Oops - too many 1 bits. + // We know the 512, 256, 128, 64, 32 bits are all set. + // Pick one at random to clear. This will break some + // checksum bits, but so be it. + println("oops", i, v) + pinfo := &pixByOff[bbit+10*i+3] // TODO random + pinfo.Contrast = 1e9 >> 8 + pinfo.HardZero = true + noops++ + } + num[i*3+0] = byte(v/100 + '0') + num[i*3+1] = byte(v/10%10 + '0') + num[i*3+2] = byte(v%10 + '0') + } + if noops > 0 { + goto Again + } + + var b1 coding.Bits + coding.String(url).Encode(&b1, p.Version) + coding.Num(num).Encode(&b1, p.Version) + b1.AddCheckBytes(p.Version, p.Level) + if !bytes.Equal(b.Bytes(), b1.Bytes()) { + fmt.Printf("mismatch\n%d %x\n%d %x\n", len(b.Bytes()), b.Bytes(), len(b1.Bytes()), b1.Bytes()) + panic("byte mismatch") + } + + cc, err := p.Encode(coding.String(url), coding.Num(num)) + if err != nil { + return err + } + + if !m.Dither { + for y, row := range expect { + for x, pix := range row { + if cc.Black(x, y) != pix { + println("mismatch", x, y, p.Pixel[y][x].String()) + } + } + } + } + + m.Code = &qr.Code{Bitmap: cc.Bitmap, Size: cc.Size, Stride: cc.Stride, Scale: m.Scale} + + if m.SaveControl { + m.Control = pngEncode(makeImage(req, "", "", 0, cc.Size, 4, m.Scale, func(x, y int) (rgba uint32) { + pix := p.Pixel[y][x] + if pix.Role() == coding.Data || pix.Role() == coding.Check { + pinfo := &pixByOff[pix.Offset()] + if pinfo.Block != nil { + if cc.Black(x, y) { + return 0x000000ff + } + return 0xffffffff + } + } + if cc.Black(x, y) { + return 0x3f3f3fff + } + return 0xbfbfbfff + })) + } + + return nil +} + +func addDither(pixByOff []Pixinfo, pix coding.Pixel, err int) { + if pix.Role() != coding.Data && pix.Role() != coding.Check { + return + } + pinfo := &pixByOff[pix.Offset()] + println("add", pinfo.X, pinfo.Y, pinfo.DTarg, err) + pinfo.DTarg += err +} + +func readTarget(name string) ([][]int, error) { + f, err := os.Open(name) + if err != nil { + return nil, err + } + m, err := png.Decode(f) + if err != nil { + return nil, fmt.Errorf("decode %s: %v", name, err) + } + rect := m.Bounds() + target := make([][]int, rect.Dy()) + for i := range target { + target[i] = make([]int, rect.Dx()) + } + for y, row := range target { + for x := range row { + a := int(color.RGBAModel.Convert(m.At(x, y)).(color.RGBA).A) + t := int(color.GrayModel.Convert(m.At(x, y)).(color.Gray).Y) + if a == 0 { + t = -1 + } + row[x] = t + } + } + return target, nil +} + +type BitBlock struct { + DataBytes int + CheckBytes int + B []byte + M [][]byte + Tmp []byte + RS *gf256.RSEncoder + bdata []byte + cdata []byte +} + +func newBlock(nd, nc int, rs *gf256.RSEncoder, dat, cdata []byte) *BitBlock { + b := &BitBlock{ + DataBytes: nd, + CheckBytes: nc, + B: make([]byte, nd+nc), + Tmp: make([]byte, nc), + RS: rs, + bdata: dat, + cdata: cdata, + } + copy(b.B, dat) + rs.ECC(b.B[:nd], b.B[nd:]) + b.check() + if !bytes.Equal(b.Tmp, cdata) { + panic("cdata") + } + + b.M = make([][]byte, nd*8) + for i := range b.M { + row := make([]byte, nd+nc) + b.M[i] = row + for j := range row { + row[j] = 0 + } + row[i/8] = 1 << (7 - uint(i%8)) + rs.ECC(row[:nd], row[nd:]) + } + return b +} + +func (b *BitBlock) check() { + b.RS.ECC(b.B[:b.DataBytes], b.Tmp) + if !bytes.Equal(b.B[b.DataBytes:], b.Tmp) { + fmt.Printf("ecc mismatch\n%x\n%x\n", b.B[b.DataBytes:], b.Tmp) + panic("mismatch") + } +} + +func (b *BitBlock) reset(bi uint, bval byte) { + if (b.B[bi/8]>>(7-bi&7))&1 == bval { + // already has desired bit + return + } + // rows that have already been set + m := b.M[len(b.M):cap(b.M)] + for _, row := range m { + if row[bi/8]&(1<<(7-bi&7)) != 0 { + // Found it. + for j, v := range row { + b.B[j] ^= v + } + return + } + } + panic("reset of unset bit") +} + +func (b *BitBlock) canSet(bi uint, bval byte) bool { + found := false + m := b.M + for j, row := range m { + if row[bi/8]&(1<<(7-bi&7)) == 0 { + continue + } + if !found { + found = true + if j != 0 { + m[0], m[j] = m[j], m[0] + } + continue + } + for k := range row { + row[k] ^= m[0][k] + } + } + if !found { + return false + } + + targ := m[0] + + // Subtract from saved-away rows too. + for _, row := range m[len(m):cap(m)] { + if row[bi/8]&(1<<(7-bi&7)) == 0 { + continue + } + for k := range row { + row[k] ^= targ[k] + } + } + + // Found a row with bit #bi == 1 and cut that bit from all the others. + // Apply to data and remove from m. + if (b.B[bi/8]>>(7-bi&7))&1 != bval { + for j, v := range targ { + b.B[j] ^= v + } + } + b.check() + n := len(m) - 1 + m[0], m[n] = m[n], m[0] + b.M = m[:n] + + for _, row := range b.M { + if row[bi/8]&(1<<(7-bi&7)) != 0 { + panic("did not reduce") + } + } + + return true +} + +func (b *BitBlock) copyOut() { + b.check() + copy(b.bdata, b.B[:b.DataBytes]) + copy(b.cdata, b.B[b.DataBytes:]) +} + +func showtable(w http.ResponseWriter, b *BitBlock, gray func(int) bool) { + nd := b.DataBytes + nc := b.CheckBytes + + fmt.Fprintf(w, "\n") + line := func() { + fmt.Fprintf(w, "\n") + for i := 0; i < (nd+nc)*8; i++ { + fmt.Fprintf(w, "> uint(7-i&7) & 1 + if gray(i) { + fmt.Fprintf(w, " class='gray'") + } + fmt.Fprintf(w, ">") + if v == 1 { + fmt.Fprintf(w, "1") + } + } + line() + } + + m := b.M[len(b.M):cap(b.M)] + for i := len(m) - 1; i >= 0; i-- { + dorow(m[i]) + } + m = b.M + for _, row := range b.M { + dorow(row) + } + + fmt.Fprintf(w, "
\n", (nd+nc)*8) + } + line() + dorow := func(row []byte) { + fmt.Fprintf(w, "
\n") +} + +func BitsTable(w http.ResponseWriter, req *http.Request) { + nd := 2 + nc := 2 + fmt.Fprintf(w, ` + + `) + rs := gf256.NewRSEncoder(coding.Field, nc) + dat := make([]byte, nd+nc) + b := newBlock(nd, nc, rs, dat[:nd], dat[nd:]) + for i := 0; i < nd*8; i++ { + b.canSet(uint(i), 0) + } + showtable(w, b, func(i int) bool { return i < nd*8 }) + + b = newBlock(nd, nc, rs, dat[:nd], dat[nd:]) + for j := 0; j < (nd+nc)*8; j += 2 { + b.canSet(uint(j), 0) + } + showtable(w, b, func(i int) bool { return i%2 == 0 }) + +} diff --git a/Godeps/_workspace/src/code.google.com/p/rsc/qr/web/resize/resize.go b/Godeps/_workspace/src/code.google.com/p/rsc/qr/web/resize/resize.go new file mode 100644 index 00000000000..02c8b004073 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/rsc/qr/web/resize/resize.go @@ -0,0 +1,152 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package resize + +import ( + "image" + "image/color" +) + +// average convert the sums to averages and returns the result. +func average(sum []uint64, w, h int, n uint64) *image.RGBA { + ret := image.NewRGBA(image.Rect(0, 0, w, h)) + for y := 0; y < h; y++ { + for x := 0; x < w; x++ { + index := 4 * (y*w + x) + pix := ret.Pix[y*ret.Stride+x*4:] + pix[0] = uint8(sum[index+0] / n) + pix[1] = uint8(sum[index+1] / n) + pix[2] = uint8(sum[index+2] / n) + pix[3] = uint8(sum[index+3] / n) + } + } + return ret +} + +// ResizeRGBA returns a scaled copy of the RGBA image slice r of m. +// The returned image has width w and height h. +func ResizeRGBA(m *image.RGBA, r image.Rectangle, w, h int) *image.RGBA { + ww, hh := uint64(w), uint64(h) + dx, dy := uint64(r.Dx()), uint64(r.Dy()) + // See comment in Resize. + n, sum := dx*dy, make([]uint64, 4*w*h) + for y := r.Min.Y; y < r.Max.Y; y++ { + pix := m.Pix[(y-r.Min.Y)*m.Stride:] + for x := r.Min.X; x < r.Max.X; x++ { + // Get the source pixel. + p := pix[(x-r.Min.X)*4:] + r64 := uint64(p[0]) + g64 := uint64(p[1]) + b64 := uint64(p[2]) + a64 := uint64(p[3]) + // Spread the source pixel over 1 or more destination rows. + py := uint64(y) * hh + for remy := hh; remy > 0; { + qy := dy - (py % dy) + if qy > remy { + qy = remy + } + // Spread the source pixel over 1 or more destination columns. + px := uint64(x) * ww + index := 4 * ((py/dy)*ww + (px / dx)) + for remx := ww; remx > 0; { + qx := dx - (px % dx) + if qx > remx { + qx = remx + } + qxy := qx * qy + sum[index+0] += r64 * qxy + sum[index+1] += g64 * qxy + sum[index+2] += b64 * qxy + sum[index+3] += a64 * qxy + index += 4 + px += qx + remx -= qx + } + py += qy + remy -= qy + } + } + } + return average(sum, w, h, n) +} + +// ResizeNRGBA returns a scaled copy of the RGBA image slice r of m. +// The returned image has width w and height h. +func ResizeNRGBA(m *image.NRGBA, r image.Rectangle, w, h int) *image.RGBA { + ww, hh := uint64(w), uint64(h) + dx, dy := uint64(r.Dx()), uint64(r.Dy()) + // See comment in Resize. + n, sum := dx*dy, make([]uint64, 4*w*h) + for y := r.Min.Y; y < r.Max.Y; y++ { + pix := m.Pix[(y-r.Min.Y)*m.Stride:] + for x := r.Min.X; x < r.Max.X; x++ { + // Get the source pixel. + p := pix[(x-r.Min.X)*4:] + r64 := uint64(p[0]) + g64 := uint64(p[1]) + b64 := uint64(p[2]) + a64 := uint64(p[3]) + r64 = (r64 * a64) / 255 + g64 = (g64 * a64) / 255 + b64 = (b64 * a64) / 255 + // Spread the source pixel over 1 or more destination rows. + py := uint64(y) * hh + for remy := hh; remy > 0; { + qy := dy - (py % dy) + if qy > remy { + qy = remy + } + // Spread the source pixel over 1 or more destination columns. + px := uint64(x) * ww + index := 4 * ((py/dy)*ww + (px / dx)) + for remx := ww; remx > 0; { + qx := dx - (px % dx) + if qx > remx { + qx = remx + } + qxy := qx * qy + sum[index+0] += r64 * qxy + sum[index+1] += g64 * qxy + sum[index+2] += b64 * qxy + sum[index+3] += a64 * qxy + index += 4 + px += qx + remx -= qx + } + py += qy + remy -= qy + } + } + } + return average(sum, w, h, n) +} + +// Resample returns a resampled copy of the image slice r of m. +// The returned image has width w and height h. +func Resample(m image.Image, r image.Rectangle, w, h int) *image.RGBA { + if w < 0 || h < 0 { + return nil + } + if w == 0 || h == 0 || r.Dx() <= 0 || r.Dy() <= 0 { + return image.NewRGBA(image.Rect(0, 0, w, h)) + } + curw, curh := r.Dx(), r.Dy() + img := image.NewRGBA(image.Rect(0, 0, w, h)) + for y := 0; y < h; y++ { + for x := 0; x < w; x++ { + // Get a source pixel. + subx := x * curw / w + suby := y * curh / h + r32, g32, b32, a32 := m.At(subx, suby).RGBA() + r := uint8(r32 >> 8) + g := uint8(g32 >> 8) + b := uint8(b32 >> 8) + a := uint8(a32 >> 8) + img.SetRGBA(x, y, color.RGBA{r, g, b, a}) + } + } + return img +} diff --git a/Godeps/_workspace/src/github.com/gokyle/hotp/LICENSE b/Godeps/_workspace/src/github.com/gokyle/hotp/LICENSE new file mode 100644 index 00000000000..208158a76a3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gokyle/hotp/LICENSE @@ -0,0 +1,13 @@ +Copyright (c) 2013 Kyle Isom + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/Godeps/_workspace/src/github.com/gokyle/hotp/README.md b/Godeps/_workspace/src/github.com/gokyle/hotp/README.md new file mode 100644 index 00000000000..b81a5acc3c6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gokyle/hotp/README.md @@ -0,0 +1,192 @@ +## `hotp` + +This package implements the RFC 4226 OATH-HOTP algorithm; these +passwords derived from the HMAC-SHA1 of an internal counter. They +are presented as (typically) 6 or 8-digit numeric passphrases. + +This package was designed to be interoperable with the Google +Authenticator app and YubiKeys programmed in OATH-HOTP mode. + +Also provided is the `hotpgen` command-line program. This generates +a QR code suitable for use with the Google Authenticator application +alongside a text file containing the URL for the QR code. For more +information, see the README in the file. + +See also the [godocs](https://godoc.org/github.com/gokyle/hotp/) +for this package. The [hotpweb](https://github.com/gokyle/hotpweb/) +package provides a simple webapp demonstrating the use of the Google +Authenticator interaction. + + +### Storing Keys + +> **These keys are cryptographic secrets.** Please store them with +> all due caution! For example, they could be encrypted in a database +> using [cryptobox](https://github.com/cryptobox/gocryptobox/). + +The HOTP keys can be serialised with the `Marshal` function; this preserves +a "snapshot", so to speak, of the key value. Serialisation is done +in DER-format: + +``` +SEQUENCE { + OCTET STRING + INTEGER + INTEGER +} +``` + +Serialised key values can be parsed with the `Unmarshal` function; +as a serialised key value is a snapshot, the counter state at the +time of marshalling will be restored. + +If the key values are to be stored in a database, the key and counter +values must be preserved. To avoid any potention issues, the counter +value should be stored using the `Counter` method (i.e., as an +`uint64`) and key values restored with `NewHOTP`. *It is strongly +recommended that the `Key` field be stored securely.* The `Digit` +field can be stored as constant in the program, and used whenever +key values are loaded. + + +### Example Usages + +#### Case 1: Google Authenticator + +A server that wants to generate a new HOTP authentication for users +can generate a new random HOTP source; this example saves a QR code +to a file. The user can scan this QR code in with the app on their +phone and use it to generate codes for the server. + + // Generate a new, random 6-digit HOTP source with an initial + // counter of 0 (the second argument, if true, will randomise + // the counter). + otp, err := GenerateHOTP(6, false) + if err != nil { + // error handling elided + } + + qrCode, err := otp.QR("user@example.net") + if err != nil { + // error handling elided + } + + err = ioutil.WriteFile("user@example.net.png", qrCode + if err != nil { + // error handling elided + } + +After the user has imported this QR code, they can immediately begin +using codes for it. The `Check` method on an OTP source will check +whether the code is valid; if it isn't, the counter won't be +decremented to prevent the server from falling out of sync. If the +either side is suspected of falling out of sync, the `Scan` method +will look ahead a certain window of values. If it finds a valid +value, the counter is updated and the two will be in sync again. + +The Google Authenticator app on Android also provides users a means +to "Check key value"; the `IntegrityCheck` method will provide the +the two values shown here (the initial code and the current counter) +that may be used to verify the integrity of the key value. + +In the `testdata` directory, there are three files with the base name +of "gauth_example" that contain the HOTP key values used in the +test suite. The PNG image may be scanned in using a mobile phone, +the text file contains the URL that the QR code is based on, and +the `.key` file may be used with the `hotpcli` program. The first +several codes produced by this URL are: + +* 023667, counter = 0 +* 641344, counter = 1 +* 419615, counter = 2 +* 692589, counter = 3 +* 237233, counter = 4 +* 711695, counter = 5 +* 620195, counter = 6 + +The codes may be checked against the app to ensure they are correct; +these values are used in the test suite to ensure interoperability. + + +#### Case 2: YubiKey + +A YubiKey programmed in "OATH-HOTP" mode can also be used with this +package. The YubiKey user will need to provide their key, and +optionally their token identifier for additional security. If the +token is used across multiple sites, the `Scan` function will need +to be used (with a probably generous window) to sync the counters +initially. + +When reading input from a YubiKey, the `YubiKey` method takes as +input the output directly from the token, and splits it into the +code, the token identity, and a boolean indicating whether it was +valid output. Note that `YubiKey` **does not** check whether the +code is correct; this ensures that the user can check the code with +whatever means is appropriate (i.e. `Scan` or `Check`). + +In the `testdata` directory, there is a configuration file containing +the paramters for the YubiKey used to generate the test suite. This +may be used to program a YubiKey to verify the package's interoperability. +The first several codes produced by this configuration are (split +into raw output from yubikey / the code / the counter): + +* cccc52345777705179, 705179, counter = 0 +* cccc52345777404068, 404068, counter = 1 +* cccc52345777490143, 490143, counter = 2 +* cccc52345777739740, 739740, counter = 3 +* cccc52345777043269, 043269, counter = 4 +* cccc52345777035666, 035666, counter = 5 +* cccc52345777725326, 725325, counter = 6 + + +### TODO + +* Add example code. + + +### Test Coverage + +Test coverage is currently at 100%. + +#### Current test status + +[![Build Status](https://drone.io/github.com/gokyle/hotp/status.png)](https://drone.io/github.com/gokyle/hotp/latest) + + +### References + +* [RFC 4226 - *HOTP: An HMAC-Based One-Time Password Algorithm*](http://www.ietf.org/rfc/rfc4226.txt) +is the specification of the OATH-HOTP algorithm. A copy is provided +in the package's source directory. + +* The [Key URI Format](https://code.google.com/p/google-authenticator/wiki/KeyUriFormat) +page on the Google Authenticator wiki documents the URI format for +use with the Google Authenticator app. This package follows that +format when generating URLs (and by extension, QR codes). + +* The [YubiKey manual](http://www.yubico.com/wp-content/uploads/2013/07/YubiKey-Manual-v3_1.pdf) +contains documentation on the YubiKey HOTP format. + + +### Author + +`hotp` was written by Kyle Isom . + + +### License + +``` +Copyright (c) 2013 Kyle Isom + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +``` diff --git a/Godeps/_workspace/src/github.com/gokyle/hotp/doc.go b/Godeps/_workspace/src/github.com/gokyle/hotp/doc.go new file mode 100644 index 00000000000..721b2ee809c --- /dev/null +++ b/Godeps/_workspace/src/github.com/gokyle/hotp/doc.go @@ -0,0 +1,27 @@ +/* +Package hotp implements the RFC 4226 OATH-HOTP algorithm; these +passwords derived from the HMAC-SHA1 of an internal counter. They +are presented as (typically) 6 or 8-digit numeric passphrases. + +The package provides facilities for interacting with YubiKeys +programmed in OATH-HOTP mode, as well as with the Google +Authenticator application. The package also provides QR-code +generation for new OTPs. +*/ +package hotp + +/* + Copyright (c) 2013 Kyle Isom + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ diff --git a/Godeps/_workspace/src/github.com/gokyle/hotp/hotp.go b/Godeps/_workspace/src/github.com/gokyle/hotp/hotp.go new file mode 100644 index 00000000000..d42f0b10c6d --- /dev/null +++ b/Godeps/_workspace/src/github.com/gokyle/hotp/hotp.go @@ -0,0 +1,315 @@ +package hotp + +import ( + "crypto/hmac" + "crypto/rand" + "crypto/sha1" + "crypto/subtle" + "encoding/asn1" + "encoding/base32" + "encoding/binary" + "errors" + "fmt" + "github.com/gravitational/teleport/Godeps/_workspace/src/code.google.com/p/rsc/qr" + "io" + "math" + "math/big" + "net/url" + "strconv" +) + +// RFC 4226 specifies the counter as being 8 bytes. +const ctrSize = 8 + +// ErrInvalidHOTPURL is returned via FromURL; it indicates a malformed +// HOTP otpauth URL. +var ErrInvalidHOTPURL = errors.New("hotp: invalid HOTP url") + +// PRNG is the source of random data; this is used by GenerateHOTP +// and should be a cryptographically-secure PRNG. +var PRNG = rand.Reader + +// HOTP represents a new key value for generating one-time passwords; +// it contains the key used to construct one-time passwords and the +// counter state used in the OTP generation. Digits contains the +// number of digits that generated OTPs should output. Key is a +// cryptographic secret, and should be treated as such. +type HOTP struct { + Key []byte + counter *[ctrSize]byte + Digits int +} + +// Counter returns the HOTP's 8-byte counter as an unsigned 64-bit +// integer. +func (otp HOTP) Counter() uint64 { + counter := binary.BigEndian.Uint64(otp.counter[:]) + return counter +} + +// Increment will increment an HOTP source's counter. This is useful +// for providers like the Google Authenticator app, which immediately +// increments the counter and uses the 0 counter value as an integrity +// check. +func (otp HOTP) Increment() { + for i := ctrSize - 1; i >= 0; i-- { + if otp.counter[i]++; otp.counter[i] != 0 { + return + } + } +} + +// OTP generates a new one-time password. +func (otp HOTP) OTP() string { + h := hmac.New(sha1.New, otp.Key) + h.Write(otp.counter[:]) + otp.Increment() + hash := h.Sum(nil) + result := truncate(hash) + + mod := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(otp.Digits)), nil) + mod = mod.Mod(big.NewInt(result), mod) + fmtStr := fmt.Sprintf("%%0%dd", otp.Digits) + return fmt.Sprintf(fmtStr, mod.Uint64()) +} + +func (otp *HOTP) setCounter(counter uint64) bool { + if otp.counter == nil { + otp.counter = new([ctrSize]byte) + } + binary.BigEndian.PutUint64(otp.counter[:], counter) + return true +} + +// NewHOTP intialises a new HOTP instance with the key and counter +// values. No check is done on the digits, but typical values are 6 +// and 8. +func NewHOTP(key []byte, counter uint64, digits int) *HOTP { + otp := &HOTP{ + Key: key, + Digits: digits, + } + otp.counter = new([ctrSize]byte) + binary.BigEndian.PutUint64(otp.counter[:], counter) + + return otp +} + +// URL returns a suitable URL, such as for the Google Authenticator +// app. The label is used by these apps to identify the service to +// which this OTP belongs. The digits value is ignored by the Google +// authenticator app, and is therefore elided in the resulting URL. +func (otp *HOTP) URL(label string) string { + secret := base32.StdEncoding.EncodeToString(otp.Key) + u := url.URL{} + v := url.Values{} + u.Scheme = "otpauth" + u.Host = "hotp" + u.Path = label + v.Add("secret", secret) + v.Add("counter", fmt.Sprintf("%d", otp.Counter())) + u.RawQuery = v.Encode() + return u.String() +} + +// QR generates a byte slice containing the a QR code encoded as a +// PNG with level Q error correction. +func (otp *HOTP) QR(label string) ([]byte, error) { + u := otp.URL(label) + code, err := qr.Encode(u, qr.Q) + if err != nil { + return nil, err + } + return code.PNG(), nil +} + +// truncate contains the DT function from the RFC; this is used to +// deterministically select a sequence of 4 bytes from the HMAC +// counter hash. +func truncate(in []byte) int64 { + offset := int(in[len(in)-1] & 0xF) + p := in[offset : offset+4] + var binCode int32 + binCode = int32((p[0] & 0x7f)) << 24 + binCode += int32((p[1] & 0xff)) << 16 + binCode += int32((p[2] & 0xff)) << 8 + binCode += int32((p[3] & 0xff)) + return int64(binCode) & 0x7FFFFFFF +} + +// FromURL parses a new HOTP from a URL string. It returns the OTP, +// the label associated with the OTP, and any errors that occurred. +func FromURL(urlString string) (*HOTP, string, error) { + u, err := url.Parse(urlString) + if err != nil { + return nil, "", err + } + + if u.Scheme != "otpauth" { + return nil, "", ErrInvalidHOTPURL + } else if u.Host != "hotp" { + return nil, "", ErrInvalidHOTPURL + } + + v := u.Query() + if len(v) == 0 { + return nil, "", ErrInvalidHOTPURL + } + if v.Get("secret") == "" { + return nil, "", ErrInvalidHOTPURL + } else if algo := v.Get("algorithm"); algo != "" && algo != "SHA1" { + return nil, "", ErrInvalidHOTPURL + } + + var identity string + if len(u.Path) > 1 { + identity = u.Path[1:] + } + + var counter uint64 + if ctr := v.Get("counter"); ctr != "" { + counter, err = strconv.ParseUint(ctr, 10, 64) + if err != nil { + return nil, "", ErrInvalidHOTPURL + } + } + + secret, err := base32.StdEncoding.DecodeString(v.Get("secret")) + if err != nil { + return nil, "", ErrInvalidHOTPURL + } + + var digits int64 = 6 + if v.Get("digits") != "" { + digits, err = strconv.ParseInt(v.Get("digits"), 10, 8) + if err != nil { + return nil, "", ErrInvalidHOTPURL + } + } + + otp := NewHOTP(secret, counter, int(digits)) + return otp, identity, nil +} + +// GenerateHOTP will generate a randomised HOTP source; if the +// randCounter parameter is true, the counter will be randomised. +func GenerateHOTP(digits int, randCounter bool) (*HOTP, error) { + key := make([]byte, sha1.Size) + _, err := io.ReadFull(PRNG, key) + if err != nil { + return nil, err + } + + var counter uint64 + if randCounter { + ctr, err := rand.Int(PRNG, big.NewInt(int64(math.MaxInt64))) + if err != nil { + return nil, err + } + counter = ctr.Uint64() + } + + return NewHOTP(key, counter, digits), nil +} + +// YubiKey reads an OATH-HOTP string as returned by a YubiKey, and +// returns three values. The first value contains the actual OTP, the +// second value contains the YubiKey's token identifier, and the final +// value indicates whether the input string was a valid YubiKey +// OTP. This does not check whether the code is correct or not, it +// only ensures that it is well-formed output from a token and +// splits the output into the code and the public identity. +func (otp *HOTP) YubiKey(in string) (string, string, bool) { + if len(in) < otp.Digits { + return "", "", false + } + + otpStart := len(in) - otp.Digits + code := in[otpStart:] + pubid := in[:otpStart] + return code, pubid, true +} + +// IntegrityCheck returns two values, the base OTP and the current +// counter. This is used, for example, with the Google Authenticator +// app's "Check key value" function and can be used to verify that +// the application and the provider are in sync. +func (otp *HOTP) IntegrityCheck() (string, uint64) { + h := hmac.New(sha1.New, otp.Key) + counter := make([]byte, 8) + h.Write(counter) + hash := h.Sum(nil) + result := truncate(hash) + + mod := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(otp.Digits)), nil) + mod = mod.Mod(big.NewInt(result), mod) + fmtStr := fmt.Sprintf("%%0%dd", otp.Digits) + return fmt.Sprintf(fmtStr, mod.Uint64()), otp.Counter() +} + +// Scan takes a code input (i.e. from the user), and scans ahead +// within a certain window of counter values. This can be used in the +// case where the server's counter and the user's counter have fallen +// out of sync. +func (otp *HOTP) Scan(code string, window int) bool { + var valid bool + codeBytes := []byte(code) + counter := otp.Counter() + + for i := 0; i < window; i++ { + genCode := []byte(otp.OTP()) + if subtle.ConstantTimeCompare(codeBytes, genCode) == 1 { + valid = true + break + } + } + if !valid { + otp.setCounter(counter) + } + return valid +} + +// Check takes an input code and verifies it against the OTP. If +// successful, the counter is incremented. +func (otp *HOTP) Check(code string) bool { + codeBytes := []byte(code) + genCode := []byte(otp.OTP()) + if subtle.ConstantTimeCompare(codeBytes, genCode) != 1 { + otp.setCounter(otp.Counter() - 1) + return false + } + return true +} + +// Marshal serialises an HOTP key value as a DER-encoded byte slice. +func Marshal(otp *HOTP) ([]byte, error) { + var asnHOTP struct { + Key []byte + Counter *big.Int + Digits int + } + asnHOTP.Key = otp.Key[:] + asnHOTP.Counter = new(big.Int).SetUint64(otp.Counter()) + asnHOTP.Digits = otp.Digits + return asn1.Marshal(asnHOTP) +} + +// Unmarshal parses a DER-encoded serialised HOTP key value. +func Unmarshal(in []byte) (otp *HOTP, err error) { + var asnHOTP struct { + Key []byte + Counter *big.Int + Digits int + } + _, err = asn1.Unmarshal(in, &asnHOTP) + if err != nil { + return + } + + otp = &HOTP{ + Key: asnHOTP.Key[:], + Digits: asnHOTP.Digits, + } + otp.setCounter(asnHOTP.Counter.Uint64()) + return +} diff --git a/Godeps/_workspace/src/github.com/gokyle/hotp/hotp_test.go b/Godeps/_workspace/src/github.com/gokyle/hotp/hotp_test.go new file mode 100644 index 00000000000..36b2c1142c0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gokyle/hotp/hotp_test.go @@ -0,0 +1,452 @@ +package hotp + +import "fmt" +import "testing" +import "bytes" +import "io" + +var testKey = []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20} + +func newZeroHOTP() *HOTP { + return NewHOTP(testKey, 0, 6) +} + +// This test verifies that the counter increment works as expected. As per the RFC, +// the counter should be treated as an 8-byte big-endian unsigned integer. +func TestIncrement(t *testing.T) { + otp := newZeroHOTP() + for i := 0; i < 32; i++ { + if otp.Counter() != uint64(i) { + fmt.Printf("hotp: counter should be %d, is %d\n", + i, otp.Counter()) + fmt.Printf("\tcounter state: %x\n", *otp.counter) + t.FailNow() + } + otp.Increment() + } +} + +var sha1Hmac = []byte{ + 0x1f, 0x86, 0x98, 0x69, 0x0e, + 0x02, 0xca, 0x16, 0x61, 0x85, + 0x50, 0xef, 0x7f, 0x19, 0xda, + 0x8e, 0x94, 0x5b, 0x55, 0x5a, +} + +var truncExpect int64 = 0x50ef7f19 + +// This test runs through the truncation example given in the RFC. +func TestTruncate(t *testing.T) { + if result := truncate(sha1Hmac); result != truncExpect { + fmt.Printf("hotp: expected truncate -> %d, saw %d\n", + truncExpect, result) + t.FailNow() + } + + sha1Hmac[19]++ + if result := truncate(sha1Hmac); result == truncExpect { + fmt.Println("hotp: expected truncation to fail") + t.FailNow() + } +} + +var rfcKey = []byte("12345678901234567890") +var rfcExpected = []string{ + "755224", + "287082", + "359152", + "969429", + "338314", + "254676", + "287922", + "162583", + "399871", + "520489", +} + +// This test runs through the test cases presented in the RFC, and +// ensures that this implementation is in compliance. +func TestRFC(t *testing.T) { + otp := NewHOTP(rfcKey, 0, 6) + for i := 0; i < len(rfcExpected); i++ { + code := otp.OTP() + if code == "" { + fmt.Printf("hotp: failed to produce an OTP\n") + t.FailNow() + } else if code != rfcExpected[i] { + fmt.Printf("hotp: invalid OTP\n") + fmt.Printf("\tExpected: %s\n", rfcExpected[i]) + fmt.Printf("\t Actual: %s\n", code) + t.FailNow() + } + } +} + +// This test uses a different key than the test cases in the RFC, +// but runs through the same test cases to ensure that they fail as +// expected. +func TestBadRFC(t *testing.T) { + otp := NewHOTP(testKey, 0, 6) + for i := 0; i < len(rfcExpected); i++ { + code := otp.OTP() + if code == "" { + fmt.Printf("hotp: failed to produce an OTP\n") + t.FailNow() + } else if code == rfcExpected[i] { + fmt.Printf("hotp: should not have received a valid OTP\n") + t.FailNow() + } + } +} + +// This test takes input from a test YubiKey and ensures that the +// YubiKey functionality works as expected. +func TestYubiKey(t *testing.T) { + ykKey := []byte{ + 0xd4, 0xbe, 0x97, 0xac, 0xe3, + 0x31, 0x72, 0x95, 0xd8, 0x95, + 0xeb, 0xd6, 0xb2, 0xec, 0xa6, + 0x78, 0x49, 0x79, 0x4d, 0xb3, + } + otp := NewHOTP(ykKey, 0, 6) + out := []string{ + "cccc52345777705179", + "cccc52345777404068", + "cccc52345777490143", + "cccc52345777739740", + "cccc52345777043269", + "cccc52345777035666", + "cccc52345777725326", + } + + codes := []string{ + "705179", + "404068", + "490143", + "739740", + "043269", + "035666", + "725326", + } + + ykpub := "cccc52345777" + + if _, _, ok := otp.YubiKey("abcd"); ok { + fmt.Println("hotp: accepted invalid YubiKey input") + t.FailNow() + } + + for i := 0; i < len(out); i++ { + code := otp.OTP() + if ykCode, pubid, ok := otp.YubiKey(out[i]); !ok { + fmt.Printf("hotp: invalid YubiKey OTP\n") + t.FailNow() + } else if ykCode != code && code != codes[i] { + fmt.Printf("hotp: YubiKey did not produce valid OTP\n") + t.FailNow() + } else if ykpub != pubid { + fmt.Printf("hotp: invalid YubiKey public ID\n") + t.FailNow() + } + } + + code, counter := otp.IntegrityCheck() + if code != codes[0] { + fmt.Println("hotp: YubiKey integrity check fails (bad code)") + t.FailNow() + } else if counter != uint64(len(out)) { + fmt.Println("hotp: YubiKey integrity check fails (bad counter)") + t.FailNow() + } +} + +// This test generates a new HOTP, outputs the URL for that HOTP, +// and attempts to parse that URL. It verifies that the two HOTPs +// are the same, and that they produce the same output. +func TestURL(t *testing.T) { + var ident = "testuser@foo" + otp := NewHOTP(testKey, 0, 6) + url := otp.URL("testuser@foo") + otp2, id, err := FromURL(url) + if err != nil { + fmt.Printf("hotp: failed to parse HOTP URL\n") + t.FailNow() + } else if id != ident { + fmt.Printf("hotp: bad label\n") + fmt.Printf("\texpected: %s\n", ident) + fmt.Printf("\t actual: %s\n", id) + t.FailNow() + } else if otp2.Counter() != otp.Counter() { + fmt.Printf("hotp: OTP counters aren't synced\n") + fmt.Printf("\toriginal: %d\n", otp.Counter()) + fmt.Printf("\t second: %d\n", otp2.Counter()) + t.FailNow() + } + + code1 := otp.OTP() + code2 := otp2.OTP() + if code1 != code2 { + fmt.Printf("hotp: mismatched OTPs\n") + fmt.Printf("\texpected: %s\n", code1) + fmt.Printf("\t actual: %s\n", code2) + } + + // There's not much we can do test the QR code, except to + // ensure it doesn't fail. + _, err = otp.QR(ident) + if err != nil { + fmt.Printf("hotp: failed to generate QR code PNG (%v)\n", err) + t.FailNow() + } + + // This should fail because the maximum size of an alphanumeric + // QR code with the lowest-level of error correction should + // max out at 4296 bytes. 8k may be a bit overkill... but it + // gets the job done. The value is read from the PRNG to + // increase the likelihood that the returned data is + // uncompressible. + var tooBigIdent = make([]byte, 8192) + _, err = io.ReadFull(PRNG, tooBigIdent) + if err != nil { + fmt.Printf("hotp: failed to read identity (%v)\n", err) + t.FailNow() + } else if _, err = otp.QR(string(tooBigIdent)); err == nil { + fmt.Println("hotp: QR code should fail to encode oversized URL") + t.FailNow() + } +} + +// This test attempts a variety of invalid urls against the parser +// to ensure they fail. +func TestBadURL(t *testing.T) { + var urlList = []string{ + "http://google.com", + "", + "-", + "foo", + "otpauth:/foo/bar/baz", + "://", + "otpauth://totp/foo@bar?secret=ABCD", + "otpauth://hotp/secret=bar", + "otpauth://hotp/?secret=QUJDRA&algorithm=SHA256", + "otpauth://hotp/?digits=", + "otpauth://hotp/?secret=123", + "otpauth://hotp/?secret=MFRGGZDF&digits=ABCD", + "otpauth://hotp/?secret=MFRGGZDF&counter=ABCD", + } + + for i := range urlList { + if _, _, err := FromURL(urlList[i]); err == nil { + fmt.Println("hotp: URL should not have parsed successfully") + fmt.Printf("\turl was: %s\n", urlList[i]) + t.FailNow() + } + } +} + +// This test uses a url generated with the `hotpgen` tool; this url +// was imported into the Google Authenticator app and the resulting +// codes generated by the app are checked here to verify interoperability. +func TestGAuth(t *testing.T) { + url := "otpauth://hotp/kyle?counter=0&secret=EXZLUP7IGHQ673ZCP32RTLRU2N427Z6L" + expected := []string{ + "023667", + "641344", + "419615", + "692589", + "237233", + "711695", + "620195", + } + + otp, label, err := FromURL(url) + if err != nil { + fmt.Printf("hotp: failed to parse HOTP URL\n") + t.FailNow() + } else if label != "kyle" { + fmt.Printf("hotp: invalid label") + t.FailNow() + } + otp.Increment() + + // Validate codes + for i := 1; i < len(expected); i++ { + code := otp.OTP() + if otp.Counter() != uint64(i+1) { + fmt.Printf("hotp: invalid OTP counter (should be %d but is %d)", + i, otp.Counter()) + t.FailNow() + } else if code != expected[i] { + fmt.Println("hotp: invalid OTP") + t.FailNow() + } + } + + // Validate integrity check + code, counter := otp.IntegrityCheck() + if code != expected[0] { + fmt.Println("hotp: invalid integrity code") + t.FailNow() + } else if counter != uint64(len(expected)) { + fmt.Println("hotp: invalid integrity counter") + t.FailNow() + } +} + +// This test verifies that a scan will successfully sync and update +// the OTP counter. +func TestScan(t *testing.T) { + url := "otpauth://hotp/kyle?counter=0&secret=EXZLUP7IGHQ673ZCP32RTLRU2N427Z6L" + expected := []string{ + "023667", + "641344", + "419615", + "692589", + "237233", + "711695", + "620195", + } + + otp, _, err := FromURL(url) + if err != nil { + fmt.Printf("hotp: failed to parse HOTP URL\n") + t.FailNow() + } + + if !otp.Scan(expected[4], 10) { + fmt.Println("hotp: scan should have found code") + t.FailNow() + } + + if otp.Counter() != 5 { + fmt.Println("hotp: counter was not properly synced") + t.FailNow() + } +} + +// This test verifies that a scan that does not successfully sync +// will not update the OTP counter. +func TestBadScan(t *testing.T) { + url := "otpauth://hotp/kyle?counter=0&secret=EXZLUP7IGHQ673ZCP32RTLRU2N427Z6L" + expected := []string{ + "023667", + "641344", + "419615", + "692589", + "237233", + "711695", + "620195", + } + + otp, _, err := FromURL(url) + if err != nil { + fmt.Printf("hotp: failed to parse HOTP URL\n") + t.FailNow() + } + + if otp.Scan(expected[6], 3) { + fmt.Println("hotp: scan should not have found code") + t.FailNow() + } + + if otp.Counter() != 0 { + fmt.Println("hotp: counter was not properly synced") + t.FailNow() + } +} + +// This test ensures that a valid code is recognised and increments +// the counter. +func TestCheck(t *testing.T) { + otp := NewHOTP(rfcKey, 0, 6) + if !otp.Check(rfcExpected[0]) { + fmt.Println("hotp: Check failed when it should have succeeded") + t.FailNow() + } + + if otp.Counter() != 1 { + fmt.Println("hotp: counter should have been incremented") + t.FailNow() + } +} + +// This test ensures that an invalid code is recognised as such and +// does not increment the counter. +func TestBadCheck(t *testing.T) { + otp := NewHOTP(rfcKey, 0, 6) + if otp.Check(rfcExpected[1]) { + fmt.Println("hotp: Check succeeded when it should have failed") + t.FailNow() + } + + if otp.Counter() != 0 { + fmt.Println("hotp: counter should not have been incremented") + t.FailNow() + } +} + +func TestSerialisation(t *testing.T) { + otp := NewHOTP(rfcKey, 123456, 8) + out, err := Marshal(otp) + if err != nil { + fmt.Printf("hotp: failed to marshal HOTP (%v)\n", err) + t.FailNow() + } + + otp2, err := Unmarshal(out) + if err != nil { + fmt.Printf("hotp: failed to unmarshal HOTP (%v)\n", err) + t.FailNow() + } + + if otp.Counter() != otp2.Counter() { + fmt.Println("hotp: serialisation failed to preserve counter") + t.FailNow() + } + + if _, err = Unmarshal([]byte{0x0}); err == nil { + fmt.Println("hotp: Unmarshal should have failed") + t.FailNow() + } +} + +func TestGenerateHOTP(t *testing.T) { + _, err := GenerateHOTP(6, false) + if err != nil { + fmt.Printf("hotp: failed to generate random key value (%v)\n", err) + t.FailNow() + } + + _, err = GenerateHOTP(8, true) + if err != nil { + fmt.Printf("hotp: failed to generate random key value (%v)\n", err) + t.FailNow() + } + + PRNG = new(bytes.Buffer) + _, err = GenerateHOTP(8, true) + if err == nil { + fmt.Println("hotp: should have failed to generate random key value") + t.FailNow() + } + + // should read just enough for the key + PRNG = bytes.NewBufferString("abcdeabcdeabcdeabcde") + _, err = GenerateHOTP(8, true) + if err == nil { + fmt.Println("hotp: should have failed to generate random key value") + t.FailNow() + } + +} + +func BenchmarkFromURL(b *testing.B) { + url := "otpauth://hotp/kyle?counter=0&secret=EXZLUP7IGHQ673ZCP32RTLRU2N427Z6L" + for i := 0; i < b.N; i++ { + _, _, err := FromURL(url) + if err != nil { + fmt.Printf("hotp: failed to parse url (%v)\n", err) + b.FailNow() + } + } +} diff --git a/Godeps/_workspace/src/github.com/gokyle/hotp/hotpcli/README b/Godeps/_workspace/src/github.com/gokyle/hotp/hotpcli/README new file mode 100644 index 00000000000..07dd3fa5528 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gokyle/hotp/hotpcli/README @@ -0,0 +1,43 @@ +hotpcli: OATH-HOTP command line tool + +hotpcli reads the DER-encoded key file specified and prints out the next code, +updating the key file with the new key. + + +Usage: + hotpcli [-cnw] [-k keyfile] [-u url] + -c Print out an integrity check; this + displays a baseline key value and + the current counter for synchronisation. + -k keyfile Specify the file containing the + key; defaults to hotp.key. + -n Don't update the counter in the key + file. + -u url An OATH-HOTP URL containing the + key-value information to use for + the key. + -w Write the url specified with -u to the + key file and exit. + +The program automatically updates the key file with the new counter; +the -n option will allow this behaviour to be overridden. To generate +a key from a url, pass the -w and -u options; the counter will not be +updated. + + +LICENSE + +Copyright (c) 2013 Kyle Isom + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + diff --git a/Godeps/_workspace/src/github.com/gokyle/hotp/hotpcli/cli.go b/Godeps/_workspace/src/github.com/gokyle/hotp/hotpcli/cli.go new file mode 100644 index 00000000000..5876156c420 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gokyle/hotp/hotpcli/cli.go @@ -0,0 +1,77 @@ +package main + +import ( + "flag" + "fmt" + "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/gokyle/hotp" + "io/ioutil" +) + +func main() { + check := flag.Bool("c", false, "do integrity check") + noUpdate := flag.Bool("n", false, "don't update counter") + keyFile := flag.String("k", "hotp.key", "key file") + url := flag.String("u", "", "URL to load new key from") + write := flag.Bool("w", false, "only write URL-loaded key to file") + flag.Parse() + + var otp *hotp.HOTP + if *url != "" { + var err error + otp, _, err = hotp.FromURL(*url) + if err != nil { + fmt.Printf("[!] %v\n", err.Error()) + return + } + + if *write { + out, err := hotp.Marshal(otp) + if err != nil { + fmt.Printf("[!] %v\n", err.Error()) + return + } + + err = ioutil.WriteFile(*keyFile, out, 0600) + if err != nil { + fmt.Printf("[!] %v\n", err.Error()) + return + } + + return + } + } else { + in, err := ioutil.ReadFile(*keyFile) + if err != nil { + fmt.Printf("[!] %v\n", err.Error()) + return + } + + otp, err = hotp.Unmarshal(in) + if err != nil { + fmt.Printf("[!] %v\n", err.Error()) + return + } + } + + if *check { + code, counter := otp.IntegrityCheck() + fmt.Println(" code:", code) + fmt.Println("counter:", counter) + } else { + fmt.Println(otp.OTP()) + } + + if !*noUpdate { + out, err := hotp.Marshal(otp) + if err != nil { + fmt.Printf("[!] %v\n", err.Error()) + return + } + + err = ioutil.WriteFile(*keyFile, out, 0600) + if err != nil { + fmt.Printf("[!] %v\n", err.Error()) + return + } + } +} diff --git a/Godeps/_workspace/src/github.com/gokyle/hotp/hotpgen/LICENSE b/Godeps/_workspace/src/github.com/gokyle/hotp/hotpgen/LICENSE new file mode 100644 index 00000000000..208158a76a3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gokyle/hotp/hotpgen/LICENSE @@ -0,0 +1,13 @@ +Copyright (c) 2013 Kyle Isom + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/Godeps/_workspace/src/github.com/gokyle/hotp/hotpgen/README b/Godeps/_workspace/src/github.com/gokyle/hotp/hotpgen/README new file mode 100644 index 00000000000..6393689502c --- /dev/null +++ b/Godeps/_workspace/src/github.com/gokyle/hotp/hotpgen/README @@ -0,0 +1,42 @@ +hotpgen: generate new HOTP key values from the command line + +Usage: + + hotpgen [-d digits] [-p password] [-r] [label] + -d digits Specify the number of digits to + use, typically 6 or 8; defaults + to 6. + + -r Randomise the initial counter; + defaults to false as this is + not supported by all clients. + + label An optional label for the key; + for example, an email address + or username. + +The program will dump out three files; a PNG image, a text file, +and a DER-encoded key file that can be used with hotpcli. The text +file contains the URL encoded in the QR code; the QR code may be +displayed to users to scan into their mobile apps. If a label is +provided, the files are named by the label (e.g. label.png and +label.txt); otherwise, the files are named as the base32-encoded +URL. + + +LICENSE + +Copyright (c) 2013 Kyle Isom + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + diff --git a/Godeps/_workspace/src/github.com/gokyle/hotp/hotpgen/hotpgen.go b/Godeps/_workspace/src/github.com/gokyle/hotp/hotpgen/hotpgen.go new file mode 100644 index 00000000000..9863ec735f8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gokyle/hotp/hotpgen/hotpgen.go @@ -0,0 +1,60 @@ +package main + +import ( + "encoding/base32" + "flag" + "fmt" + "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/gokyle/hotp" + "io/ioutil" +) + +func main() { + digits := flag.Int("d", 6, "number of digits") + doRand := flag.Bool("r", false, "randomise counter") + flag.Parse() + + var label string + if flag.NArg() == 1 { + label = flag.Arg(0) + } + + otp, err := hotp.GenerateHOTP(*digits, *doRand) + if err != nil { + fmt.Printf("! %v\n", err.Error()) + return + } + + url := otp.URL(label) + png, err := otp.QR(label) + if err != nil { + fmt.Printf("! %v\n", err.Error()) + return + } + + filename := label + if label == "" { + filename = base32.StdEncoding.EncodeToString([]byte(url)) + } + err = ioutil.WriteFile(filename+".png", png, 0644) + if err != nil { + fmt.Printf("! %v\n", err.Error()) + return + } + + err = ioutil.WriteFile(filename+".txt", []byte(url), 0644) + if err != nil { + fmt.Printf("! %v\n", err.Error()) + return + } + + keyFile, err := hotp.Marshal(otp) + if err != nil { + fmt.Printf("! %v\n", err.Error()) + return + } + err = ioutil.WriteFile(filename+".key", keyFile, 0644) + if err != nil { + fmt.Printf("! %v\n", err.Error()) + return + } +} diff --git a/Godeps/_workspace/src/github.com/gokyle/hotp/rfc4226.txt b/Godeps/_workspace/src/github.com/gokyle/hotp/rfc4226.txt new file mode 100644 index 00000000000..b852364fa85 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gokyle/hotp/rfc4226.txt @@ -0,0 +1,2075 @@ + + + + + + +Network Working Group D. M'Raihi +Request for Comments: 4226 VeriSign +Category: Informational M. Bellare + UCSD + F. Hoornaert + Vasco + D. Naccache + Gemplus + O. Ranen + Aladdin + December 2005 + + + HOTP: An HMAC-Based One-Time Password Algorithm + +Status of This Memo + + This memo provides information for the Internet community. It does + not specify an Internet standard of any kind. Distribution of this + memo is unlimited. + +Copyright Notice + + Copyright (C) The Internet Society (2005). + +Abstract + + This document describes an algorithm to generate one-time password + values, based on Hashed Message Authentication Code (HMAC). A + security analysis of the algorithm is presented, and important + parameters related to the secure deployment of the algorithm are + discussed. The proposed algorithm can be used across a wide range of + network applications ranging from remote Virtual Private Network + (VPN) access, Wi-Fi network logon to transaction-oriented Web + applications. + + This work is a joint effort by the OATH (Open AuTHentication) + membership to specify an algorithm that can be freely distributed to + the technical community. The authors believe that a common and + shared algorithm will facilitate adoption of two-factor + authentication on the Internet by enabling interoperability across + commercial and open-source implementations. + + + + + + + + + +M'Raihi, et al. Informational [Page 1] + +RFC 4226 HOTP Algorithm December 2005 + + +Table of Contents + + 1. Overview ........................................................3 + 2. Introduction ....................................................3 + 3. Requirements Terminology ........................................4 + 4. Algorithm Requirements ..........................................4 + 5. HOTP Algorithm ..................................................5 + 5.1. Notation and Symbols .......................................5 + 5.2. Description ................................................6 + 5.3. Generating an HOTP Value ...................................6 + 5.4. Example of HOTP Computation for Digit = 6 ..................7 + 6. Security Considerations .........................................8 + 7. Security Requirements ...........................................9 + 7.1. Authentication Protocol Requirements .......................9 + 7.2. Validation of HOTP Values .................................10 + 7.3. Throttling at the Server ..................................10 + 7.4. Resynchronization of the Counter ..........................11 + 7.5. Management of Shared Secrets ..............................11 + 8. Composite Shared Secrets .......................................14 + 9. Bi-Directional Authentication ..................................14 + 10. Conclusion ....................................................15 + 11. Acknowledgements ..............................................15 + 12. Contributors ..................................................15 + 13. References ....................................................15 + 13.1. Normative References .....................................15 + 13.2. Informative References ...................................16 + Appendix A - HOTP Algorithm Security: Detailed Analysis ...........17 + A.1. Definitions and Notations .................................17 + A.2. The Idealized Algorithm: HOTP-IDEAL .......................17 + A.3. Model of Security .........................................18 + A.4. Security of the Ideal Authentication Algorithm ............19 + A.4.1. From Bits to Digits ................................19 + A.4.2. Brute Force Attacks ................................21 + A.4.3. Brute force attacks are the best possible attacks ..22 + A.5. Security Analysis of HOTP .................................23 + Appendix B - SHA-1 Attacks ........................................25 + B.1. SHA-1 Status ..............................................25 + B.2. HMAC-SHA-1 Status .........................................26 + B.3. HOTP Status ...............................................26 + Appendix C - HOTP Algorithm: Reference Implementation .............27 + Appendix D - HOTP Algorithm: Test Values ..........................32 + Appendix E - Extensions ...........................................33 + E.1. Number of Digits ..........................................33 + E.2. Alphanumeric Values .......................................33 + E.3. Sequence of HOTP values ...................................34 + E.4. A Counter-Based Resynchronization Method ..................34 + E.5. Data Field ................................................35 + + + + +M'Raihi, et al. Informational [Page 2] + +RFC 4226 HOTP Algorithm December 2005 + + +1. Overview + + The document introduces first the context around an algorithm that + generates one-time password values based on HMAC [BCK1] and, thus, is + named the HMAC-Based One-Time Password (HOTP) algorithm. In Section + 4, the algorithm requirements are listed and in Section 5, the HOTP + algorithm is described. Sections 6 and 7 focus on the algorithm + security. Section 8 proposes some extensions and improvements, and + Section 10 concludes this document. In Appendix A, the interested + reader will find a detailed, full-fledged analysis of the algorithm + security: an idealized version of the algorithm is evaluated, and + then the HOTP algorithm security is analyzed. + +2. Introduction + + Today, deployment of two-factor authentication remains extremely + limited in scope and scale. Despite increasingly higher levels of + threats and attacks, most Internet applications still rely on weak + authentication schemes for policing user access. The lack of + interoperability among hardware and software technology vendors has + been a limiting factor in the adoption of two-factor authentication + technology. In particular, the absence of open specifications has + led to solutions where hardware and software components are tightly + coupled through proprietary technology, resulting in high-cost + solutions, poor adoption, and limited innovation. + + In the last two years, the rapid rise of network threats has exposed + the inadequacies of static passwords as the primary mean of + authentication on the Internet. At the same time, the current + approach that requires an end user to carry an expensive, single- + function device that is only used to authenticate to the network is + clearly not the right answer. For two-factor authentication to + propagate on the Internet, it will have to be embedded in more + flexible devices that can work across a wide range of applications. + + The ability to embed this base technology while ensuring broad + interoperability requires that it be made freely available to the + broad technical community of hardware and software developers. Only + an open-system approach will ensure that basic two-factor + authentication primitives can be built into the next generation of + consumer devices such as USB mass storage devices, IP phones, and + personal digital assistants. + + One-Time Password is certainly one of the simplest and most popular + forms of two-factor authentication for securing network access. For + example, in large enterprises, Virtual Private Network access often + requires the use of One-Time Password tokens for remote user + authentication. One-Time Passwords are often preferred to stronger + + + +M'Raihi, et al. Informational [Page 3] + +RFC 4226 HOTP Algorithm December 2005 + + + forms of authentication such as Public-Key Infrastructure (PKI) or + biometrics because an air-gap device does not require the + installation of any client desktop software on the user machine, + therefore allowing them to roam across multiple machines including + home computers, kiosks, and personal digital assistants. + + This document proposes a simple One-Time Password algorithm that can + be implemented by any hardware manufacturer or software developer to + create interoperable authentication devices and software agents. The + algorithm is event-based so that it can be embedded in high-volume + devices such as Java smart cards, USB dongles, and GSM SIM cards. + The presented algorithm is made freely available to the developer + community under the terms and conditions of the IETF Intellectual + Property Rights [RFC3979]. + + The authors of this document are members of the Open AuTHentication + initiative [OATH]. The initiative was created in 2004 to facilitate + collaboration among strong authentication technology providers. + +3. Requirements Terminology + + The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", + "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this + document are to be interpreted as described in [RFC2119]. + +4. Algorithm Requirements + + This section presents the main requirements that drove this algorithm + design. A lot of emphasis was placed on end-consumer usability as + well as the ability for the algorithm to be implemented by low-cost + hardware that may provide minimal user interface capabilities. In + particular, the ability to embed the algorithm into high-volume SIM + and Java cards was a fundamental prerequisite. + + R1 - The algorithm MUST be sequence- or counter-based: one of the + goals is to have the HOTP algorithm embedded in high-volume devices + such as Java smart cards, USB dongles, and GSM SIM cards. + + R2 - The algorithm SHOULD be economical to implement in hardware by + minimizing requirements on battery, number of buttons, computational + horsepower, and size of LCD display. + + R3 - The algorithm MUST work with tokens that do not support any + numeric input, but MAY also be used with more sophisticated devices + such as secure PIN-pads. + + R4 - The value displayed on the token MUST be easily read and entered + by the user: This requires the HOTP value to be of reasonable length. + + + +M'Raihi, et al. Informational [Page 4] + +RFC 4226 HOTP Algorithm December 2005 + + + The HOTP value must be at least a 6-digit value. It is also + desirable that the HOTP value be 'numeric only' so that it can be + easily entered on restricted devices such as phones. + + R5 - There MUST be user-friendly mechanisms available to + resynchronize the counter. Section 7.4 and Appendix E.4 details the + resynchronization mechanism proposed in this document + + R6 - The algorithm MUST use a strong shared secret. The length of + the shared secret MUST be at least 128 bits. This document + RECOMMENDs a shared secret length of 160 bits. + +5. HOTP Algorithm + + In this section, we introduce the notation and describe the HOTP + algorithm basic blocks -- the base function to compute an HMAC-SHA-1 + value and the truncation method to extract an HOTP value. + +5.1. Notation and Symbols + + A string always means a binary string, meaning a sequence of zeros + and ones. + + If s is a string, then |s| denotes its length. + + If n is a number, then |n| denotes its absolute value. + + If s is a string, then s[i] denotes its i-th bit. We start numbering + the bits at 0, so s = s[0]s[1]...s[n-1] where n = |s| is the length + of s. + + Let StToNum (String to Number) denote the function that as input a + string s returns the number whose binary representation is s. (For + example, StToNum(110) = 6.) + + Here is a list of symbols used in this document. + + Symbol Represents + ------------------------------------------------------------------- + C 8-byte counter value, the moving factor. This counter + MUST be synchronized between the HOTP generator (client) + and the HOTP validator (server). + + K shared secret between client and server; each HOTP + generator has a different and unique secret K. + + T throttling parameter: the server will refuse connections + from a user after T unsuccessful authentication attempts. + + + +M'Raihi, et al. Informational [Page 5] + +RFC 4226 HOTP Algorithm December 2005 + + + + s resynchronization parameter: the server will attempt to + verify a received authenticator across s consecutive + counter values. + + Digit number of digits in an HOTP value; system parameter. + +5.2. Description + + The HOTP algorithm is based on an increasing counter value and a + static symmetric key known only to the token and the validation + service. In order to create the HOTP value, we will use the HMAC- + SHA-1 algorithm, as defined in RFC 2104 [BCK2]. + + As the output of the HMAC-SHA-1 calculation is 160 bits, we must + truncate this value to something that can be easily entered by a + user. + + HOTP(K,C) = Truncate(HMAC-SHA-1(K,C)) + + Where: + + - Truncate represents the function that converts an HMAC-SHA-1 + value into an HOTP value as defined in Section 5.3. + + The Key (K), the Counter (C), and Data values are hashed high-order + byte first. + + The HOTP values generated by the HOTP generator are treated as big + endian. + +5.3. Generating an HOTP Value + + We can describe the operations in 3 distinct steps: + + Step 1: Generate an HMAC-SHA-1 value Let HS = HMAC-SHA-1(K,C) // HS + is a 20-byte string + + Step 2: Generate a 4-byte string (Dynamic Truncation) + Let Sbits = DT(HS) // DT, defined below, + // returns a 31-bit string + + Step 3: Compute an HOTP value + Let Snum = StToNum(Sbits) // Convert S to a number in + 0...2^{31}-1 + Return D = Snum mod 10^Digit // D is a number in the range + 0...10^{Digit}-1 + + + + +M'Raihi, et al. Informational [Page 6] + +RFC 4226 HOTP Algorithm December 2005 + + + The Truncate function performs Step 2 and Step 3, i.e., the dynamic + truncation and then the reduction modulo 10^Digit. The purpose of + the dynamic offset truncation technique is to extract a 4-byte + dynamic binary code from a 160-bit (20-byte) HMAC-SHA-1 result. + + DT(String) // String = String[0]...String[19] + Let OffsetBits be the low-order 4 bits of String[19] + Offset = StToNum(OffsetBits) // 0 <= OffSet <= 15 + Let P = String[OffSet]...String[OffSet+3] + Return the Last 31 bits of P + + The reason for masking the most significant bit of P is to avoid + confusion about signed vs. unsigned modulo computations. Different + processors perform these operations differently, and masking out the + signed bit removes all ambiguity. + + Implementations MUST extract a 6-digit code at a minimum and possibly + 7 and 8-digit code. Depending on security requirements, Digit = 7 or + more SHOULD be considered in order to extract a longer HOTP value. + + The following paragraph is an example of using this technique for + Digit = 6, i.e., that a 6-digit HOTP value is calculated from the + HMAC value. + +5.4. Example of HOTP Computation for Digit = 6 + + The following code example describes the extraction of a dynamic + binary code given that hmac_result is a byte array with the HMAC- + SHA-1 result: + + int offset = hmac_result[19] & 0xf ; + int bin_code = (hmac_result[offset] & 0x7f) << 24 + | (hmac_result[offset+1] & 0xff) << 16 + | (hmac_result[offset+2] & 0xff) << 8 + | (hmac_result[offset+3] & 0xff) ; + + SHA-1 HMAC Bytes (Example) + + ------------------------------------------------------------- + | Byte Number | + ------------------------------------------------------------- + |00|01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|16|17|18|19| + ------------------------------------------------------------- + | Byte Value | + ------------------------------------------------------------- + |1f|86|98|69|0e|02|ca|16|61|85|50|ef|7f|19|da|8e|94|5b|55|5a| + -------------------------------***********----------------++| + + + + +M'Raihi, et al. Informational [Page 7] + +RFC 4226 HOTP Algorithm December 2005 + + + * The last byte (byte 19) has the hex value 0x5a. + * The value of the lower 4 bits is 0xa (the offset value). + * The offset value is byte 10 (0xa). + * The value of the 4 bytes starting at byte 10 is 0x50ef7f19, + which is the dynamic binary code DBC1. + * The MSB of DBC1 is 0x50 so DBC2 = DBC1 = 0x50ef7f19 . + * HOTP = DBC2 modulo 10^6 = 872921. + + We treat the dynamic binary code as a 31-bit, unsigned, big-endian + integer; the first byte is masked with a 0x7f. + + We then take this number modulo 1,000,000 (10^6) to generate the 6- + digit HOTP value 872921 decimal. + +6. Security Considerations + + The conclusion of the security analysis detailed in the Appendix is + that, for all practical purposes, the outputs of the Dynamic + Truncation (DT) on distinct counter inputs are uniformly and + independently distributed 31-bit strings. + + The security analysis then details the impact of the conversion from + a string to an integer and the final reduction modulo 10^Digit, where + Digit is the number of digits in an HOTP value. + + The analysis demonstrates that these final steps introduce a + negligible bias, which does not impact the security of the HOTP + algorithm, in the sense that the best possible attack against the + HOTP function is the brute force attack. + + Assuming an adversary is able to observe numerous protocol exchanges + and collect sequences of successful authentication values. This + adversary, trying to build a function F to generate HOTP values based + on his observations, will not have a significant advantage over a + random guess. + + The logical conclusion is simply that the best strategy will once + again be to perform a brute force attack to enumerate and try all the + possible values. + + Considering the security analysis in the Appendix of this document, + without loss of generality, we can approximate closely the security + of the HOTP algorithm by the following formula: + + Sec = sv/10^Digit + + + + + + +M'Raihi, et al. Informational [Page 8] + +RFC 4226 HOTP Algorithm December 2005 + + + Where: + - Sec is the probability of success of the adversary; + - s is the look-ahead synchronization window size; + - v is the number of verification attempts; + - Digit is the number of digits in HOTP values. + + Obviously, we can play with s, T (the Throttling parameter that would + limit the number of attempts by an attacker), and Digit until + achieving a certain level of security, still preserving the system + usability. + +7. Security Requirements + + Any One-Time Password algorithm is only as secure as the application + and the authentication protocols that implement it. Therefore, this + section discusses the critical security requirements that our choice + of algorithm imposes on the authentication protocol and validation + software. + + The parameters T and s discussed in this section have a significant + impact on the security -- further details in Section 6 elaborate on + the relations between these parameters and their impact on the system + security. + + It is also important to remark that the HOTP algorithm is not a + substitute for encryption and does not provide for the privacy of + data transmission. Other mechanisms should be used to defeat attacks + aimed at breaking confidentiality and privacy of transactions. + +7.1. Authentication Protocol Requirements + + We introduce in this section some requirements for a protocol P + implementing HOTP as the authentication method between a prover and a + verifier. + + RP1 - P MUST support two-factor authentication, i.e., the + communication and verification of something you know (secret code + such as a Password, Pass phrase, PIN code, etc.) and something you + have (token). The secret code is known only to the user and usually + entered with the One-Time Password value for authentication purpose + (two-factor authentication). + + RP2 - P SHOULD NOT be vulnerable to brute force attacks. This + implies that a throttling/lockout scheme is RECOMMENDED on the + validation server side. + + RP3 - P SHOULD be implemented over a secure channel in order to + protect users' privacy and avoid replay attacks. + + + +M'Raihi, et al. Informational [Page 9] + +RFC 4226 HOTP Algorithm December 2005 + + +7.2. Validation of HOTP Values + + The HOTP client (hardware or software token) increments its counter + and then calculates the next HOTP value HOTP client. If the value + received by the authentication server matches the value calculated by + the client, then the HOTP value is validated. In this case, the + server increments the counter value by one. + + If the value received by the server does not match the value + calculated by the client, the server initiate the resynch protocol + (look-ahead window) before it requests another pass. + + If the resynch fails, the server asks then for another + authentication pass of the protocol to take place, until the + maximum number of authorized attempts is reached. + + If and when the maximum number of authorized attempts is reached, the + server SHOULD lock out the account and initiate a procedure to inform + the user. + +7.3. Throttling at the Server + + Truncating the HMAC-SHA-1 value to a shorter value makes a brute + force attack possible. Therefore, the authentication server needs to + detect and stop brute force attacks. + + We RECOMMEND setting a throttling parameter T, which defines the + maximum number of possible attempts for One-Time Password validation. + The validation server manages individual counters per HOTP device in + order to take note of any failed attempt. We RECOMMEND T not to be + too large, particularly if the resynchronization method used on the + server is window-based, and the window size is large. T SHOULD be + set as low as possible, while still ensuring that usability is not + significantly impacted. + + Another option would be to implement a delay scheme to avoid a brute + force attack. After each failed attempt A, the authentication server + would wait for an increased T*A number of seconds, e.g., say T = 5, + then after 1 attempt, the server waits for 5 seconds, at the second + failed attempt, it waits for 5*2 = 10 seconds, etc. + + The delay or lockout schemes MUST be across login sessions to prevent + attacks based on multiple parallel guessing techniques. + + + + + + + + +M'Raihi, et al. Informational [Page 10] + +RFC 4226 HOTP Algorithm December 2005 + + +7.4. Resynchronization of the Counter + + Although the server's counter value is only incremented after a + successful HOTP authentication, the counter on the token is + incremented every time a new HOTP is requested by the user. Because + of this, the counter values on the server and on the token might be + out of synchronization. + + We RECOMMEND setting a look-ahead parameter s on the server, which + defines the size of the look-ahead window. In a nutshell, the server + can recalculate the next s HOTP-server values, and check them against + the received HOTP client. + + Synchronization of counters in this scenario simply requires the + server to calculate the next HOTP values and determine if there is a + match. Optionally, the system MAY require the user to send a + sequence of (say, 2, 3) HOTP values for resynchronization purpose, + since forging a sequence of consecutive HOTP values is even more + difficult than guessing a single HOTP value. + + The upper bound set by the parameter s ensures the server does not go + on checking HOTP values forever (causing a denial-of-service attack) + and also restricts the space of possible solutions for an attacker + trying to manufacture HOTP values. s SHOULD be set as low as + possible, while still ensuring that usability is not impacted. + +7.5. Management of Shared Secrets + + The operations dealing with the shared secrets used to generate and + verify OTP values must be performed securely, in order to mitigate + risks of any leakage of sensitive information. We describe in this + section different modes of operations and techniques to perform these + different operations with respect to the state of the art in data + security. + + We can consider two different avenues for generating and storing + (securely) shared secrets in the Validation system: + + * Deterministic Generation: secrets are derived from a master + seed, both at provisioning and verification stages and generated + on-the-fly whenever it is required. + * Random Generation: secrets are generated randomly at + provisioning stage and must be stored immediately and kept + secure during their life cycle. + + + + + + + +M'Raihi, et al. Informational [Page 11] + +RFC 4226 HOTP Algorithm December 2005 + + + Deterministic Generation + ------------------------ + + A possible strategy is to derive the shared secrets from a master + secret. The master secret will be stored at the server only. A + tamper-resistant device MUST be used to store the master key and + derive the shared secrets from the master key and some public + information. The main benefit would be to avoid the exposure of the + shared secrets at any time and also avoid specific requirements on + storage, since the shared secrets could be generated on-demand when + needed at provisioning and validation time. + + We distinguish two different cases: + + - A single master key MK is used to derive the shared secrets; + each HOTP device has a different secret, K_i = SHA-1 (MK,i) + where i stands for a public piece of information that identifies + uniquely the HOTP device such as a serial number, a token ID, + etc. Obviously, this is in the context of an application or + service -- different application or service providers will have + different secrets and settings. + - Several master keys MK_i are used and each HOTP device stores a + set of different derived secrets, {K_i,j = SHA-1(MK_i,j)} where + j stands for a public piece of information identifying the + device. The idea would be to store ONLY the active master key + at the validation server, in the Hardware Security Module (HSM), + and keep in a safe place, using secret sharing methods such as + [Shamir] for instance. In this case, if a master secret MK_i is + compromised, then it is possible to switch to another secret + without replacing all the devices. + + The drawback in the deterministic case is that the exposure of the + master secret would obviously enable an attacker to rebuild any + shared secret based on correct public information. The revocation of + all secrets would be required, or switching to a new set of secrets + in the case of multiple master keys. + + On the other hand, the device used to store the master key(s) and + generate the shared secrets MUST be tamper resistant. Furthermore, + the HSM will not be exposed outside the security perimeter of the + validation system, therefore reducing the risk of leakage. + + + + + + + + + + +M'Raihi, et al. Informational [Page 12] + +RFC 4226 HOTP Algorithm December 2005 + + + Random Generation + ----------------- + + The shared secrets are randomly generated. We RECOMMEND following + the recommendations in [RFC4086] and selecting a good and secure + random source for generating these secrets. A (true) random + generator requires a naturally occurring source of randomness. + Practically, there are two possible avenues to consider for the + generation of the shared secrets: + + * Hardware-based generators: they exploit the randomness that + occurs in physical phenomena. A nice implementation can be based on + oscillators and built in such ways that active attacks are more + difficult to perform. + + * Software-based generators: designing a good software random + generator is not an easy task. A simple, but efficient, + implementation should be based on various sources and apply to the + sampled sequence a one-way function such as SHA-1. + + We RECOMMEND selecting proven products, being hardware or software + generators, for the computation of shared secrets. + + We also RECOMMEND storing the shared secrets securely, and more + specifically encrypting the shared secrets when stored using tamper- + resistant hardware encryption and exposing them only when required: + for example, the shared secret is decrypted when needed to verify an + HOTP value, and re-encrypted immediately to limit exposure in the RAM + for a short period of time. The data store holding the shared + secrets MUST be in a secure area, to avoid as much as possible direct + attack on the validation system and secrets database. + + Particularly, access to the shared secrets should be limited to + programs and processes required by the validation system only. We + will not elaborate on the different security mechanisms to put in + place, but obviously, the protection of shared secrets is of the + uttermost importance. + + + + + + + + + + + + + + +M'Raihi, et al. Informational [Page 13] + +RFC 4226 HOTP Algorithm December 2005 + + +8. Composite Shared Secrets + + It may be desirable to include additional authentication factors in + the shared secret K. These additional factors can consist of any + data known at the token but not easily obtained by others. Examples + of such data include: + + * PIN or Password obtained as user input at the token + * Phone number + * Any unique identifier programmatically available at the token + + In this scenario, the composite shared secret K is constructed during + the provisioning process from a random seed value combined with one + or more additional authentication factors. The server could either + build on-demand or store composite secrets -- in any case, depending + on implementation choice, the token only stores the seed value. When + the token performs the HOTP calculation, it computes K from the seed + value and the locally derived or input values of the other + authentication factors. + + The use of composite shared secrets can strengthen HOTP-based + authentication systems through the inclusion of additional + authentication factors at the token. To the extent that the token is + a trusted device, this approach has the further benefit of not + requiring exposure of the authentication factors (such as the user + input PIN) to other devices. + +9. Bi-Directional Authentication + + Interestingly enough, the HOTP client could also be used to + authenticate the validation server, claiming that it is a genuine + entity knowing the shared secret. + + Since the HOTP client and the server are synchronized and share the + same secret (or a method to recompute it), a simple 3-pass protocol + could be put in place: + 1- The end user enter the TokenID and a first OTP value OTP1; + 2- The server checks OTP1 and if correct, sends back OTP2; + 3- The end user checks OTP2 using his HOTP device and if correct, + uses the web site. + + Obviously, as indicated previously, all the OTP communications have + to take place over a secure channel, e.g., SSL/TLS, IPsec + connections. + + + + + + + +M'Raihi, et al. Informational [Page 14] + +RFC 4226 HOTP Algorithm December 2005 + + +10. Conclusion + + This document describes HOTP, a HMAC-based One-Time Password + algorithm. It also recommends the preferred implementation and + related modes of operations for deploying the algorithm. + + The document also exhibits elements of security and demonstrates that + the HOTP algorithm is practical and sound, the best possible attack + being a brute force attack that can be prevented by careful + implementation of countermeasures in the validation server. + + Eventually, several enhancements have been proposed, in order to + improve security if needed for specific applications. + +11. Acknowledgements + + The authors would like to thank Siddharth Bajaj, Alex Deacon, Loren + Hart, and Nico Popp for their help during the conception and + redaction of this document. + +12. Contributors + + The authors of this document would like to emphasize the role of + three persons who have made a key contribution to this document: + + - Laszlo Elteto is system architect with SafeNet, Inc. + + - Ernesto Frutos is director of Engineering with Authenex, Inc. + + - Fred McClain is Founder and CTO with Boojum Mobile, Inc. + + Without their advice and valuable inputs, this document would not be + the same. + +13. References + +13.1. Normative References + + [BCK1] M. Bellare, R. Canetti and H. Krawczyk, "Keyed Hash + Functions and Message Authentication", Proceedings of + Crypto'96, LNCS Vol. 1109, pp. 1-15. + + [BCK2] Krawczyk, H., Bellare, M., and R. Canetti, "HMAC: Keyed- + Hashing for Message Authentication", RFC 2104, February + 1997. + + [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate + Requirement Levels", BCP 14, RFC 2119, March 1997. + + + +M'Raihi, et al. Informational [Page 15] + +RFC 4226 HOTP Algorithm December 2005 + + + [RFC3979] Bradner, S., "Intellectual Property Rights in IETF + Technology", BCP 79, RFC 3979, March 2005. + + [RFC4086] Eastlake, D., 3rd, Schiller, J., and S. Crocker, + "Randomness Requirements for Security", BCP 106, RFC 4086, + June 2005. + +13.2. Informative References + + [OATH] Initiative for Open AuTHentication + http://www.openauthentication.org + + [PrOo] B. Preneel and P. van Oorschot, "MD-x MAC and building + fast MACs from hash functions", Advances in Cryptology + CRYPTO '95, Lecture Notes in Computer Science Vol. 963, D. + Coppersmith ed., Springer-Verlag, 1995. + + [Crack] Crack in SHA-1 code 'stuns' security gurus + http://www.eetimes.com/showArticle.jhtml? + articleID=60402150 + + [Sha1] Bruce Schneier. SHA-1 broken. February 15, 2005. + http://www.schneier.com/blog/archives/2005/02/ + sha1_broken.html + + [Res] Researchers: Digital encryption standard flawed + http://news.com.com/ + Researchers+Digital+encryption+standard+flawed/ + 2100-1002-5579881.html?part=dht&tag=ntop&tag=nl.e703 + + [Shamir] How to Share a Secret, by Adi Shamir. In Communications + of the ACM, Vol. 22, No. 11, pp. 612-613, November, 1979. + + + + + + + + + + + + + + + + + + + +M'Raihi, et al. Informational [Page 16] + +RFC 4226 HOTP Algorithm December 2005 + + +Appendix A - HOTP Algorithm Security: Detailed Analysis + + The security analysis of the HOTP algorithm is summarized in this + section. We first detail the best attack strategies, and then + elaborate on the security under various assumptions and the impact of + the truncation and make some recommendations regarding the number of + digits. + + We focus this analysis on the case where Digit = 6, i.e., an HOTP + function that produces 6-digit values, which is the bare minimum + recommended in this document. + +A.1. Definitions and Notations + + We denote by {0,1}^l the set of all strings of length l. + + Let Z_{n} = {0,.., n - 1}. + + Let IntDiv(a,b) denote the integer division algorithm that takes + input integers a, b where a >= b >= 1 and returns integers (q,r) + + the quotient and remainder, respectively, of the division of a by b. + (Thus, a = bq + r and 0 <= r < b.) + + Let H: {0,1}^k x {0,1}^c --> {0,1}^n be the base function that takes + a k-bit key K and c-bit counter C and returns an n-bit output H(K,C). + (In the case of HOTP, H is HMAC-SHA-1; we use this formal definition + for generalizing our proof of security.) + +A.2. The Idealized Algorithm: HOTP-IDEAL + + We now define an idealized counterpart of the HOTP algorithm. In + this algorithm, the role of H is played by a random function that + forms the key. + + To be more precise, let Maps(c,n) denote the set of all functions + mapping from {0,1}^c to {0,1}^n. The idealized algorithm has key + space Maps(c,n), so that a "key" for such an algorithm is a function + h from {0,1}^c to {0,1}^n. We imagine this key (function) to be + drawn at random. It is not feasible to implement this idealized + algorithm, since the key, being a function from {0,1}^c to {0,1}^n, + is way too large to even store. So why consider it? + + Our security analysis will show that as long as H satisfies a certain + well-accepted assumption, the security of the actual and idealized + algorithms is for all practical purposes the same. The task that + really faces us, then, is to assess the security of the idealized + algorithm. + + + +M'Raihi, et al. Informational [Page 17] + +RFC 4226 HOTP Algorithm December 2005 + + + In analyzing the idealized algorithm, we are concentrating on + assessing the quality of the design of the algorithm itself, + independently of HMAC-SHA-1. This is in fact the important issue. + +A.3. Model of Security + + The model exhibits the type of threats or attacks that are being + considered and enables one to assess the security of HOTP and HOTP- + IDEAL. We denote ALG as either HOTP or HOTP-IDEAL for the purpose of + this security analysis. + + The scenario we are considering is that a user and server share a key + K for ALG. Both maintain a counter C, initially zero, and the user + authenticates itself by sending ALG(K,C) to the server. The latter + accepts if this value is correct. + + In order to protect against accidental increment of the user counter, + the server, upon receiving a value z, will accept as long as z equals + ALG(K,i) for some i in the range C,...,C + s-1, where s is the + resynchronization parameter and C is the server counter. If it + accepts with some value of i, it then increments its counter to i+1. + If it does not accept, it does not change its counter value. + + The model we specify captures what an adversary can do and what it + needs to achieve in order to "win". First, the adversary is assumed + to be able to eavesdrop, meaning, to see the authenticator + transmitted by the user. Second, the adversary wins if it can get + the server to accept an authenticator relative to a counter value for + which the user has never transmitted an authenticator. + + The formal adversary, which we denote by B, starts out knowing which + algorithm ALG is being used, knowing the system design, and knowing + all system parameters. The one and only thing it is not given a + priori is the key K shared between the user and the server. + + The model gives B full control of the scheduling of events. It has + access to an authenticator oracle representing the user. By calling + this oracle, the adversary can ask the user to authenticate itself + and get back the authenticator in return. It can call this oracle as + often as it wants and when it wants, using the authenticators it + accumulates to perhaps "learn" how to make authenticators itself. At + any time, it may also call a verification oracle, supplying the + latter with a candidate authenticator of its choice. It wins if the + server accepts this accumulator. + + Consider the following game involving an adversary B that is + attempting to compromise the security of an authentication algorithm + ALG: K x {0,1}^c --> R. + + + +M'Raihi, et al. Informational [Page 18] + +RFC 4226 HOTP Algorithm December 2005 + + + Initializations - A key K is selected at random from K, a counter C + is initialized to 0, and the Boolean value win is set to false. + + Game execution - Adversary B is provided with the two following + oracles: + + Oracle AuthO() + -------------- + A = ALG(K,C) + C = C + 1 + Return O to B + + Oracle VerO(A) + -------------- + i = C + While (i <= C + s - 1 and Win == FALSE) do + If A == ALG(K,i) then Win = TRUE; C = i + 1 + Else i = i + 1 + Return Win to B + + AuthO() is the authenticator oracle and VerO(A) is the verification + oracle. + + Upon execution, B queries the two oracles at will. Let Adv(B) be the + probability that win gets set to true in the above game. This is the + probability that the adversary successfully impersonates the user. + + Our goal is to assess how large this value can be as a function of + the number v of verification queries made by B, the number a of + authenticator oracle queries made by B, and the running time t of B. + This will tell us how to set the throttle, which effectively upper + bounds v. + +A.4. Security of the Ideal Authentication Algorithm + + This section summarizes the security analysis of HOTP-IDEAL, starting + with the impact of the conversion modulo 10^Digit and then focusing + on the different possible attacks. + +A.4.1. From Bits to Digits + + The dynamic offset truncation of a random n-bit string yields a + random 31-bit string. What happens to the distribution when it is + taken modulo m = 10^Digit, as done in HOTP? + + + + + + + +M'Raihi, et al. Informational [Page 19] + +RFC 4226 HOTP Algorithm December 2005 + + + The following lemma estimates the biases in the outputs in this case. + + Lemma 1 + ------- + Let N >= m >= 1 be integers, and let (q,r) = IntDiv(N,m). For z in + Z_{m} let: + + P_{N,m}(z) = Pr [x mod m = z : x randomly pick in Z_{n}] + + Then for any z in Z_{m} + + P_{N,m}(z) = (q + 1) / N if 0 <= z < r + q / N if r <= z < m + + Proof of Lemma 1 + ---------------- + Let the random variable X be uniformly distributed over Z_{N}. Then: + + P_{N,m}(z) = Pr [X mod m = z] + + = Pr [X < mq] * Pr [X mod m = z| X < mq] + + Pr [mq <= X < N] * Pr [X mod m = z| mq <= X < N] + + = mq/N * 1/m + + (N - mq)/N * 1 / (N - mq) if 0 <= z < N - mq + 0 if N - mq <= z <= m + + = q/N + + r/N * 1 / r if 0 <= z < N - mq + 0 if r <= z <= m + + Simplifying yields the claimed equation. + + Let N = 2^31, d = 6, and m = 10^d. If x is chosen at random from + Z_{N} (meaning, is a random 31-bit string), then reducing it to a 6- + digit number by taking x mod m does not yield a random 6-digit + number. + + Rather, x mod m is distributed as shown in the following table: + + Values Probability that each appears as output + ---------------------------------------------------------------- + 0,1,...,483647 2148/2^31 roughly equals to 1.00024045/10^6 + 483648,...,999999 2147/2^31 roughly equals to 0.99977478/10^6 + + If X is uniformly distributed over Z_{2^31} (meaning, is a random + 31-bit string), then the above shows the probabilities for different + outputs of X mod 10^6. The first set of values appears with + + + +M'Raihi, et al. Informational [Page 20] + +RFC 4226 HOTP Algorithm December 2005 + + + probability slightly greater than 10^-6, the rest with probability + slightly less, meaning that the distribution is slightly non-uniform. + + However, as the table above indicates, the bias is small, and as we + will see later, negligible: the probabilities are very close to + 10^-6. + +A.4.2. Brute Force Attacks + + If the authenticator consisted of d random digits, then a brute force + attack using v verification attempts would succeed with probability + sv/10^Digit. + + However, an adversary can exploit the bias in the outputs of + HOTP-IDEAL, predicted by Lemma 1, to mount a slightly better attack. + + Namely, it makes authentication attempts with authenticators that are + the most likely values, meaning the ones in the range 0,...,r - 1, + where (q,r) = IntDiv(2^31,10^Digit). + + The following specifies an adversary in our model of security that + mounts the attack. It estimates the success probability as a + function of the number of verification queries. + + For simplicity, we assume that the number of verification queries is + at most r. With N = 2^31 and m = 10^6, we have r = 483,648, and the + throttle value is certainly less than this, so this assumption is not + much of a restriction. + + Proposition 1 + ------------- + + Suppose m = 10^Digit < 2^31, and let (q,r) = IntDiv(2^31,m). Assume + s <= m. The brute-force-attack adversary B-bf attacks HOTP using v + <= r verification oracle queries. This adversary makes no + authenticator oracle queries, and succeeds with probability + + Adv(B-bf) = 1 - (1 - v(q+1)/2^31)^s + + which is roughly equal to + + sv * (q+1)/2^31 + + With m = 10^6 we get q = 2,147. In that case, the brute force attack + using v verification attempts succeeds with probability + + Adv(B-bf) roughly = sv * 2148/2^31 = sv * 1.00024045/10^6 + + + + +M'Raihi, et al. Informational [Page 21] + +RFC 4226 HOTP Algorithm December 2005 + + + As this equation shows, the resynchronization parameter s has a + significant impact in that the adversary's success probability is + proportional to s. This means that s cannot be made too large + without compromising security. + +A.4.3. Brute force attacks are the best possible attacks. + + A central question is whether there are attacks any better than the + brute force one. In particular, the brute force attack did not + attempt to collect authenticators sent by the user and try to + cryptanalyze them in an attempt to learn how to better construct + authenticators. Would doing this help? Is there some way to "learn" + how to build authenticators that result in a higher success rate than + given by the brute-force attack? + + The following says the answer to these questions is no. No matter + what strategy the adversary uses, and even if it sees, and tries to + exploit, the authenticators from authentication attempts of the user, + its success probability will not be above that of the brute force + attack -- this is true as long as the number of authentications it + observes is not incredibly large. This is valuable information + regarding the security of the scheme. + + Proposition 2 ------------- Suppose m = 10^Digit < 2^31, and let + (q,r) = IntDiv(2^31,m). Let B be any adversary attacking HOTP-IDEAL + using v verification oracle queries and a <= 2^c - s authenticator + oracle queries. Then + + Adv(B) < = sv * (q+1)/ 2^31 + + Note: This result is conditional on the adversary not seeing more + than 2^c - s authentications performed by the user, which is hardly + restrictive as long as c is large enough. + + With m = 10^6, we get q = 2,147. In that case, Proposition 2 says + that any adversary B attacking HOTP-IDEAL and making v verification + attempts succeeds with probability at most + + Equation 1 + ---------- + sv * 2148/2^31 roughly = sv * 1.00024045/10^6 + + Meaning, B's success rate is not more than that achieved by the brute + force attack. + + + + + + + +M'Raihi, et al. Informational [Page 22] + +RFC 4226 HOTP Algorithm December 2005 + + +A.5. Security Analysis of HOTP + + We have analyzed, in the previous sections, the security of the + idealized counterparts HOTP-IDEAL of the actual authentication + algorithm HOTP. We now show that, under appropriate and well- + believed assumption on H, the security of the actual algorithms is + essentially the same as that of its idealized counterpart. + + The assumption in question is that H is a secure pseudorandom + function, or PRF, meaning that its input-output values are + indistinguishable from those of a random function in practice. + + Consider an adversary A that is given an oracle for a function f: + {0,1}^c --> {0, 1}^n and eventually outputs a bit. We denote Adv(A) + as the prf-advantage of A, which represents how well the adversary + does at distinguishing the case where its oracle is H(K,.) from the + case where its oracle is a random function of {0,1}^c to {0,1}^n. + + One possible attack is based on exhaustive search for the key K. If + A runs for t steps and T denotes the time to perform one computation + of H, its prf-advantage from this attack turns out to be (t/T)2^-k. + Another possible attack is a birthday one [PrOo], whereby A can + attain advantage p^2/2^n in p oracle queries and running time about + pT. + + Our assumption is that these are the best possible attacks. This + translates into the following. + + Assumption 1 + ------------ + + Let T denotes the time to perform one computation of H. Then if A is + any adversary with running time at most t and making at most p oracle + queries, + + Adv(A) <= (t/T)/2^k + p^2/2^n + + In practice, this assumption means that H is very secure as PRF. For + example, given that k = n = 160, an attacker with running time 2^60 + and making 2^40 oracle queries has advantage at most (about) 2^-80. + + Theorem 1 + --------- + + Suppose m = 10^Digit < 2^31, and let (q,r) = IntDiv(2^31,m). Let B + be any adversary attacking HOTP using v verification oracle queries, + + + + + +M'Raihi, et al. Informational [Page 23] + +RFC 4226 HOTP Algorithm December 2005 + + + a <= 2^c - s authenticator oracle queries, and running time t. Let T + denote the time to perform one computation of H. If Assumption 1 is + true, then + + Adv(B) <= sv * (q + 1)/2^31 + (t/T)/2^k + ((sv + a)^2)/2^n + + In practice, the (t/T)2^-k + ((sv + a)^2)2^-n term is much smaller + than the sv(q + 1)/2^n term, so that the above says that for all + practical purposes the success rate of an adversary attacking HOTP is + sv(q + 1)/2^n, just as for HOTP-IDEAL, meaning the HOTP algorithm is + in practice essentially as good as its idealized counterpart. + + In the case m = 10^6 of a 6-digit output, this means that an + adversary making v authentication attempts will have a success rate + that is at most that of Equation 1. + + For example, consider an adversary with running time at most 2^60 + that sees at most 2^40 authentication attempts of the user. Both + these choices are very generous to the adversary, who will typically + not have these resources, but we are saying that even such a powerful + adversary will not have more success than indicated by Equation 1. + + We can safely assume sv <= 2^40 due to the throttling and bounds on + s. So: + + (t/T)/2^k + ((sv + a)^2)/2^n <= 2^60/2^160 + (2^41)^2/2^160 + roughly <= 2^-78 + + which is much smaller than the success probability of Equation 1 and + negligible compared to it. + + + + + + + + + + + + + + + + + + + + + +M'Raihi, et al. Informational [Page 24] + +RFC 4226 HOTP Algorithm December 2005 + + +Appendix B - SHA-1 Attacks + + This sections addresses the impact of the recent attacks on SHA-1 on + the security of the HMAC-SHA-1-based HOTP. We begin with some + discussion of the situation of SHA-1 and then discuss the relevance + to HMAC-SHA-1 and HOTP. Cited references are in Section 13. + +B.1. SHA-1 Status + + A collision for a hash function h means a pair x,y of different + inputs such that h(x)=h(y). Since SHA-1 outputs 160 bits, a birthday + attack finds a collision in 2^{80} trials. (A trial means one + computation of the function.) This was thought to be the best + possible until Wang, Yin, and Yu announced on February 15, 2005, that + they had an attack finding collisions in 2^{69} trials. + + Is SHA-1 broken? For most practical purposes, we would say probably + not, since the resources needed to mount the attack are huge. Here + is one way to get a sense of it: we can estimate it is about the same + as the time we would need to factor a 760-bit RSA modulus, and this + is currently considered out of reach. + + Burr of NIST is quoted in [Crack] as saying "Large national + intelligence agencies could do this in a reasonable amount of time + with a few million dollars in computer time". However, the + computation may be out of reach of all but such well-funded agencies. + + One should also ask what impact finding SHA-1 collisions actually has + on security of real applications such as signatures. To exploit a + collision x,y to forge signatures, you need to somehow obtain a + signature of x and then you can forge a signature of y. How damaging + this is depends on the content of y: the y created by the attack may + not be meaningful in the application context. Also, one needs a + chosen-message attack to get the signature of x. This seems possible + in some contexts, but not others. Overall, it is not clear that the + impact on the security of signatures is significant. + + Indeed, one can read in the press that SHA-1 is "broken" [Sha1] and + that encryption and SSL are "broken" [Res]. The media have a + tendency to magnify events: it would hardly be interesting to + announce in the news that a team of cryptanalysts did very + interesting theoretical work in attacking SHA-1. + + Cryptographers are excited too. But mainly because this is an + important theoretical breakthrough. Attacks can only get better with + time: it is therefore important to monitor any progress in hash + functions cryptanalysis and be prepared for any really practical + break with a sound migration plan for the future. + + + +M'Raihi, et al. Informational [Page 25] + +RFC 4226 HOTP Algorithm December 2005 + + +B.2. HMAC-SHA-1 Status + + The new attacks on SHA-1 have no impact on the security of + HMAC-SHA-1. The best attack on the latter remains one needing a + sender to authenticate 2^{80} messages before an adversary can create + a forgery. Why? + + HMAC is not a hash function. It is a message authentication code + (MAC) that uses a hash function internally. A MAC depends on a + secret key, while hash functions don't. What one needs to worry + about with a MAC is forgery, not collisions. HMAC was designed so + that collisions in the hash function (here SHA-1) do not yield + forgeries for HMAC. + + Recall that HMAC-SHA-1(K,x) = SHA-1(K_o,SHA-1(K_i,x)) where the keys + K_o,K_i are derived from K. Suppose the attacker finds a pair x,y + such that SHA-1(K_i,x) = SHA-1(K_i,y). (Call this a hidden-key + collision.) Then if it can obtain the MAC of x (itself a tall + order), it can forge the MAC of y. (These values are the same.) But + finding hidden-key collisions is harder than finding collisions, + because the attacker does not know the hidden key K_i. All it may + have is some outputs of HMAC-SHA-1 with key K. To date, there are no + claims or evidence that the recent attacks on SHA-1 extend to find + hidden-key collisions. + + Historically, the HMAC design has already proven itself in this + regard. MD5 is considered broken in that collisions in this hash + function can be found relatively easily. But there is still no + attack on HMAC-MD5 better than the trivial 2^{64} time birthday one. + (MD5 outputs 128 bits, not 160.) We are seeing this strength of HMAC + coming into play again in the SHA-1 context. + +B.3. HOTP Status + + Since no new weakness has surfaced in HMAC-SHA-1, there is no impact + on HOTP. The best attacks on HOTP remain those described in the + document, namely, to try to guess output values. + + The security proof of HOTP requires that HMAC-SHA-1 behave like a + pseudorandom function. The quality of HMAC-SHA-1 as a pseudorandom + function is not impacted by the new attacks on SHA-1, and so neither + is this proven guarantee. + + + + + + + + + +M'Raihi, et al. Informational [Page 26] + +RFC 4226 HOTP Algorithm December 2005 + + +Appendix C - HOTP Algorithm: Reference Implementation + + /* + * OneTimePasswordAlgorithm.java + * OATH Initiative, + * HOTP one-time password algorithm + * + */ + + /* Copyright (C) 2004, OATH. All rights reserved. + * + * License to copy and use this software is granted provided that it + * is identified as the "OATH HOTP Algorithm" in all material + * mentioning or referencing this software or this function. + * + * License is also granted to make and use derivative works provided + * that such works are identified as + * "derived from OATH HOTP algorithm" + * in all material mentioning or referencing the derived work. + * + * OATH (Open AuTHentication) and its members make no + * representations concerning either the merchantability of this + * software or the suitability of this software for any particular + * purpose. + * + * It is provided "as is" without express or implied warranty + * of any kind and OATH AND ITS MEMBERS EXPRESSaLY DISCLAIMS + * ANY WARRANTY OR LIABILITY OF ANY KIND relating to this software. + * + * These notices must be retained in any copies of any part of this + * documentation and/or software. + */ + + package org.openauthentication.otp; + + import java.io.IOException; + import java.io.File; + import java.io.DataInputStream; + import java.io.FileInputStream ; + import java.lang.reflect.UndeclaredThrowableException; + + import java.security.GeneralSecurityException; + import java.security.NoSuchAlgorithmException; + import java.security.InvalidKeyException; + + import javax.crypto.Mac; + import javax.crypto.spec.SecretKeySpec; + + + + +M'Raihi, et al. Informational [Page 27] + +RFC 4226 HOTP Algorithm December 2005 + + + /** + * This class contains static methods that are used to calculate the + * One-Time Password (OTP) using + * JCE to provide the HMAC-SHA-1. + * + * @author Loren Hart + * @version 1.0 + */ + public class OneTimePasswordAlgorithm { + private OneTimePasswordAlgorithm() {} + + // These are used to calculate the check-sum digits. + // 0 1 2 3 4 5 6 7 8 9 + private static final int[] doubleDigits = + { 0, 2, 4, 6, 8, 1, 3, 5, 7, 9 }; + + /** + * Calculates the checksum using the credit card algorithm. + * This algorithm has the advantage that it detects any single + * mistyped digit and any single transposition of + * adjacent digits. + * + * @param num the number to calculate the checksum for + * @param digits number of significant places in the number + * + * @return the checksum of num + */ + public static int calcChecksum(long num, int digits) { + boolean doubleDigit = true; + int total = 0; + while (0 < digits--) { + int digit = (int) (num % 10); + num /= 10; + if (doubleDigit) { + digit = doubleDigits[digit]; + } + total += digit; + doubleDigit = !doubleDigit; + } + int result = total % 10; + if (result > 0) { + result = 10 - result; + } + return result; + } + + /** + * This method uses the JCE to provide the HMAC-SHA-1 + + + +M'Raihi, et al. Informational [Page 28] + +RFC 4226 HOTP Algorithm December 2005 + + + * algorithm. + * HMAC computes a Hashed Message Authentication Code and + * in this case SHA1 is the hash algorithm used. + * + * @param keyBytes the bytes to use for the HMAC-SHA-1 key + * @param text the message or text to be authenticated. + * + * @throws NoSuchAlgorithmException if no provider makes + * either HmacSHA1 or HMAC-SHA-1 + * digest algorithms available. + * @throws InvalidKeyException + * The secret provided was not a valid HMAC-SHA-1 key. + * + */ + + public static byte[] hmac_sha1(byte[] keyBytes, byte[] text) + throws NoSuchAlgorithmException, InvalidKeyException + { + // try { + Mac hmacSha1; + try { + hmacSha1 = Mac.getInstance("HmacSHA1"); + } catch (NoSuchAlgorithmException nsae) { + hmacSha1 = Mac.getInstance("HMAC-SHA-1"); + } + SecretKeySpec macKey = + new SecretKeySpec(keyBytes, "RAW"); + hmacSha1.init(macKey); + return hmacSha1.doFinal(text); + // } catch (GeneralSecurityException gse) { + // throw new UndeclaredThrowableException(gse); + // } + } + + private static final int[] DIGITS_POWER + // 0 1 2 3 4 5 6 7 8 + = {1,10,100,1000,10000,100000,1000000,10000000,100000000}; + + /** + * This method generates an OTP value for the given + * set of parameters. + * + * @param secret the shared secret + * @param movingFactor the counter, time, or other value that + * changes on a per use basis. + * @param codeDigits the number of digits in the OTP, not + * including the checksum, if any. + * @param addChecksum a flag that indicates if a checksum digit + + + +M'Raihi, et al. Informational [Page 29] + +RFC 4226 HOTP Algorithm December 2005 + + + * should be appended to the OTP. + * @param truncationOffset the offset into the MAC result to + * begin truncation. If this value is out of + * the range of 0 ... 15, then dynamic + * truncation will be used. + * Dynamic truncation is when the last 4 + * bits of the last byte of the MAC are + * used to determine the start offset. + * @throws NoSuchAlgorithmException if no provider makes + * either HmacSHA1 or HMAC-SHA-1 + * digest algorithms available. + * @throws InvalidKeyException + * The secret provided was not + * a valid HMAC-SHA-1 key. + * + * @return A numeric String in base 10 that includes + * {@link codeDigits} digits plus the optional checksum + * digit if requested. + */ + static public String generateOTP(byte[] secret, + long movingFactor, + int codeDigits, + boolean addChecksum, + int truncationOffset) + throws NoSuchAlgorithmException, InvalidKeyException + { + // put movingFactor value into text byte array + String result = null; + int digits = addChecksum ? (codeDigits + 1) : codeDigits; + byte[] text = new byte[8]; + for (int i = text.length - 1; i >= 0; i--) { + text[i] = (byte) (movingFactor & 0xff); + movingFactor >>= 8; + } + + // compute hmac hash + byte[] hash = hmac_sha1(secret, text); + + // put selected bytes into result int + int offset = hash[hash.length - 1] & 0xf; + if ( (0<=truncationOffset) && + (truncationOffset<(hash.length-4)) ) { + offset = truncationOffset; + } + int binary = + ((hash[offset] & 0x7f) << 24) + | ((hash[offset + 1] & 0xff) << 16) + | ((hash[offset + 2] & 0xff) << 8) + + + +M'Raihi, et al. Informational [Page 30] + +RFC 4226 HOTP Algorithm December 2005 + + + | (hash[offset + 3] & 0xff); + + int otp = binary % DIGITS_POWER[codeDigits]; + if (addChecksum) { + otp = (otp * 10) + calcChecksum(otp, codeDigits); + } + result = Integer.toString(otp); + while (result.length() < digits) { + result = "0" + result; + } + return result; + } + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +M'Raihi, et al. Informational [Page 31] + +RFC 4226 HOTP Algorithm December 2005 + + +Appendix D - HOTP Algorithm: Test Values + + The following test data uses the ASCII string + "12345678901234567890" for the secret: + + Secret = 0x3132333435363738393031323334353637383930 + + Table 1 details for each count, the intermediate HMAC value. + + Count Hexadecimal HMAC-SHA-1(secret, count) + 0 cc93cf18508d94934c64b65d8ba7667fb7cde4b0 + 1 75a48a19d4cbe100644e8ac1397eea747a2d33ab + 2 0bacb7fa082fef30782211938bc1c5e70416ff44 + 3 66c28227d03a2d5529262ff016a1e6ef76557ece + 4 a904c900a64b35909874b33e61c5938a8e15ed1c + 5 a37e783d7b7233c083d4f62926c7a25f238d0316 + 6 bc9cd28561042c83f219324d3c607256c03272ae + 7 a4fb960c0bc06e1eabb804e5b397cdc4b45596fa + 8 1b3c89f65e6c9e883012052823443f048b4332db + 9 1637409809a679dc698207310c8c7fc07290d9e5 + + Table 2 details for each count the truncated values (both in + hexadecimal and decimal) and then the HOTP value. + + Truncated + Count Hexadecimal Decimal HOTP + 0 4c93cf18 1284755224 755224 + 1 41397eea 1094287082 287082 + 2 82fef30 137359152 359152 + 3 66ef7655 1726969429 969429 + 4 61c5938a 1640338314 338314 + 5 33c083d4 868254676 254676 + 6 7256c032 1918287922 287922 + 7 4e5b397 82162583 162583 + 8 2823443f 673399871 399871 + 9 2679dc69 645520489 520489 + + + + + + + + + + + + + + + +M'Raihi, et al. Informational [Page 32] + +RFC 4226 HOTP Algorithm December 2005 + + +Appendix E - Extensions + + + We introduce in this section several enhancements to the HOTP + algorithm. These are not recommended extensions or part of the + standard algorithm, but merely variations that could be used for + customized implementations. + +E.1. Number of Digits + + A simple enhancement in terms of security would be to extract more + digits from the HMAC-SHA-1 value. + + For instance, calculating the HOTP value modulo 10^8 to build an 8- + digit HOTP value would reduce the probability of success of the + adversary from sv/10^6 to sv/10^8. + + This could give the opportunity to improve usability, e.g., by + increasing T and/or s, while still achieving a better security + overall. For instance, s = 10 and 10v/10^8 = v/10^7 < v/10^6 which + is the theoretical optimum for 6-digit code when s = 1. + +E.2. Alphanumeric Values + + Another option is to use A-Z and 0-9 values; or rather a subset of 32 + symbols taken from the alphanumerical alphabet in order to avoid any + confusion between characters: 0, O, and Q as well as l, 1, and I are + very similar, and can look the same on a small display. + + The immediate consequence is that the security is now in the order of + sv/32^6 for a 6-digit HOTP value and sv/32^8 for an 8-digit HOTP + value. + + 32^6 > 10^9 so the security of a 6-alphanumeric HOTP code is slightly + better than a 9-digit HOTP value, which is the maximum length of an + HOTP code supported by the proposed algorithm. + + 32^8 > 10^12 so the security of an 8-alphanumeric HOTP code is + significantly better than a 9-digit HOTP value. + + Depending on the application and token/interface used for displaying + and entering the HOTP value, the choice of alphanumeric values could + be a simple and efficient way to improve security at a reduced cost + and impact on users. + + + + + + + +M'Raihi, et al. Informational [Page 33] + +RFC 4226 HOTP Algorithm December 2005 + + +E.3. Sequence of HOTP Values + + As we suggested for the resynchronization to enter a short sequence + (say, 2 or 3) of HOTP values, we could generalize the concept to the + protocol, and add a parameter L that would define the length of the + HOTP sequence to enter. + + Per default, the value L SHOULD be set to 1, but if security needs to + be increased, users might be asked (possibly for a short period of + time, or a specific operation) to enter L HOTP values. + + This is another way, without increasing the HOTP length or using + alphanumeric values to tighten security. + + Note: The system MAY also be programmed to request synchronization on + a regular basis (e.g., every night, twice a week, etc.) and to + achieve this purpose, ask for a sequence of L HOTP values. + +E.4. A Counter-Based Resynchronization Method + + In this case, we assume that the client can access and send not only + the HOTP value but also other information, more specifically, the + counter value. + + A more efficient and secure method for resynchronization is possible + in this case. The client application will not send the HOTP-client + value only, but the HOTP-client and the related C-client counter + value, the HOTP value acting as a message authentication code of the + counter. + + Resynchronization Counter-based Protocol (RCP) + ---------------------------------------------- + + The server accepts if the following are all true, where C-server is + its own current counter value: + + 1) C-client >= C-server + 2) C-client - C-server <= s + 3) Check that HOTP client is valid HOTP(K,C-Client) + 4) If true, the server sets C to C-client + 1 and client is + authenticated + + In this case, there is no need for managing a look-ahead window + anymore. The probability of success of the adversary is only v/10^6 + or roughly v in one million. A side benefit is obviously to be able + to increase s "infinitely" and therefore improve the system usability + without impacting the security. + + + + +M'Raihi, et al. Informational [Page 34] + +RFC 4226 HOTP Algorithm December 2005 + + + This resynchronization protocol SHOULD be used whenever the related + impact on the client and server applications is deemed acceptable. + +E.5. Data Field + + Another interesting option is the introduction of a Data field, which + would be used for generating the One-Time Password values: HOTP (K, + C, [Data]) where Data is an optional field that can be the + concatenation of various pieces of identity-related information, + e.g., Data = Address | PIN. + + We could also use a Timer, either as the only moving factor or in + combination with the Counter -- in this case, e.g., Data = Timer, + where Timer could be the UNIX-time (GMT seconds since 1/1/1970) + divided by some factor (8, 16, 32, etc.) in order to give a specific + time step. The time window for the One-Time Password is then equal + to the time step multiplied by the resynchronization parameter as + defined before. For example, if we take 64 seconds as the time step + and 7 for the resynchronization parameter, we obtain an acceptance + window of +/- 3 minutes. + + Using a Data field opens for more flexibility in the algorithm + implementation, provided that the Data field is clearly specified. + + + + + + + + + + + + + + + + + + + + + + + + + + + + +M'Raihi, et al. Informational [Page 35] + +RFC 4226 HOTP Algorithm December 2005 + + +Authors' Addresses + + David M'Raihi (primary contact for sending comments and questions) + VeriSign, Inc. + 685 E. Middlefield Road + Mountain View, CA 94043 USA + + Phone: 1-650-426-3832 + EMail: dmraihi@verisign.com + + + Mihir Bellare + Dept of Computer Science and Engineering, Mail Code 0114 + University of California at San Diego + 9500 Gilman Drive + La Jolla, CA 92093, USA + + EMail: mihir@cs.ucsd.edu + + + Frank Hoornaert + VASCO Data Security, Inc. + Koningin Astridlaan 164 + 1780 Wemmel, Belgium + + EMail: frh@vasco.com + + + David Naccache + Gemplus Innovation + 34 rue Guynemer, 92447, + Issy les Moulineaux, France + and + Information Security Group, + Royal Holloway, + University of London, Egham, + Surrey TW20 0EX, UK + + EMail: david.naccache@gemplus.com, david.naccache@rhul.ac.uk + + + Ohad Ranen + Aladdin Knowledge Systems Ltd. + 15 Beit Oved Street + Tel Aviv, Israel 61110 + + EMail: Ohad.Ranen@ealaddin.com + + + + +M'Raihi, et al. Informational [Page 36] + +RFC 4226 HOTP Algorithm December 2005 + + +Full Copyright Statement + + Copyright (C) The Internet Society (2005). + + This document is subject to the rights, licenses and restrictions + contained in BCP 78, and except as set forth therein, the authors + retain all their rights. + + This document and the information contained herein are provided on an + "AS IS" basis and THE CONTRIBUTOR, THE ORGANIZATION HE/SHE REPRESENTS + OR IS SPONSORED BY (IF ANY), THE INTERNET SOCIETY AND THE INTERNET + ENGINEERING TASK FORCE DISCLAIM ALL WARRANTIES, EXPRESS OR IMPLIED, + INCLUDING BUT NOT LIMITED TO ANY WARRANTY THAT THE USE OF THE + INFORMATION HEREIN WILL NOT INFRINGE ANY RIGHTS OR ANY IMPLIED + WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. + +Intellectual Property + + The IETF takes no position regarding the validity or scope of any + Intellectual Property Rights or other rights that might be claimed to + pertain to the implementation or use of the technology described in + this document or the extent to which any license under such rights + might or might not be available; nor does it represent that it has + made any independent effort to identify any such rights. Information + on the procedures with respect to rights in RFC documents can be + found in BCP 78 and BCP 79. + + Copies of IPR disclosures made to the IETF Secretariat and any + assurances of licenses to be made available, or the result of an + attempt made to obtain a general license or permission for the use of + such proprietary rights by implementers or users of this + specification can be obtained from the IETF on-line IPR repository at + http://www.ietf.org/ipr. + + The IETF invites any interested party to bring to its attention any + copyrights, patents or patent applications, or other proprietary + rights that may cover technology that may be required to implement + this standard. Please address the information to the IETF at ietf- + ipr@ietf.org. + +Acknowledgement + + Funding for the RFC Editor function is currently provided by the + Internet Society. + + + + + + + +M'Raihi, et al. Informational [Page 37] + diff --git a/Godeps/_workspace/src/github.com/gokyle/hotp/testdata/gauth_example.key b/Godeps/_workspace/src/github.com/gokyle/hotp/testdata/gauth_example.key new file mode 100644 index 00000000000..629a279d721 Binary files /dev/null and b/Godeps/_workspace/src/github.com/gokyle/hotp/testdata/gauth_example.key differ diff --git a/Godeps/_workspace/src/github.com/gokyle/hotp/testdata/gauth_example.png b/Godeps/_workspace/src/github.com/gokyle/hotp/testdata/gauth_example.png new file mode 100644 index 00000000000..3adeef9e97d Binary files /dev/null and b/Godeps/_workspace/src/github.com/gokyle/hotp/testdata/gauth_example.png differ diff --git a/Godeps/_workspace/src/github.com/gokyle/hotp/testdata/gauth_example.txt b/Godeps/_workspace/src/github.com/gokyle/hotp/testdata/gauth_example.txt new file mode 100644 index 00000000000..1c83848e74a --- /dev/null +++ b/Godeps/_workspace/src/github.com/gokyle/hotp/testdata/gauth_example.txt @@ -0,0 +1 @@ +otpauth://hotp/kyle?counter=0&secret=EXZLUP7IGHQ673ZCP32RTLRU2N427Z6L \ No newline at end of file diff --git a/Godeps/_workspace/src/github.com/gokyle/hotp/testdata/yubikey.csv b/Godeps/_workspace/src/github.com/gokyle/hotp/testdata/yubikey.csv new file mode 100644 index 00000000000..93f644b7a69 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gokyle/hotp/testdata/yubikey.csv @@ -0,0 +1,2 @@ +LOGGING START,12/17/13 4:16 PM +OATH-HOTP,12/17/13 4:16 PM,1,ccccgdefgiii,,d4be97ace3317295d895ebd6b2eca67849794db3,,,0,1,0,6,0,0,0,0,0,0 diff --git a/Godeps/_workspace/src/github.com/gravitational/orbit/lib/utils/kv.go b/Godeps/_workspace/src/github.com/gravitational/orbit/lib/utils/kv.go deleted file mode 100644 index 1677e9d96ff..00000000000 --- a/Godeps/_workspace/src/github.com/gravitational/orbit/lib/utils/kv.go +++ /dev/null @@ -1,43 +0,0 @@ -package utils - -import ( - "bytes" - "fmt" - "strings" - - "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/gravitational/trace" - "github.com/gravitational/teleport/Godeps/_workspace/src/gopkg.in/alecthomas/kingpin.v2" -) - -// KeyVal is a key value flag-compatible data structure -type KeyVal map[string]string - -// Set accepts string with arguments in the form "key:val,key2:val2" -func (kv *KeyVal) Set(v string) error { - for _, i := range SplitComma(v) { - vals := strings.SplitN(i, ":", 2) - if len(vals) != 2 { - return trace.Errorf("extra options should be defined like KEY:VAL") - } - (*kv)[vals[0]] = vals[1] - } - return nil -} - -// String returns a string with comma separated key-values: "key:val,key2:val2" -func (kv *KeyVal) String() string { - b := &bytes.Buffer{} - for k, v := range *kv { - fmt.Fprintf(b, "%v:%v", k, v) - fmt.Fprintf(b, " ") - } - return b.String() -} - -// KeyValParam accepts a kingpin setting parameter and returns -// kingpin-compatible value -func KeyValParam(s kingpin.Settings) *KeyVal { - kv := make(KeyVal) - s.SetValue(&kv) - return &kv -} diff --git a/Godeps/_workspace/src/github.com/gravitational/orbit/lib/utils/split.go b/Godeps/_workspace/src/github.com/gravitational/orbit/lib/utils/split.go deleted file mode 100644 index 7c3add7775f..00000000000 --- a/Godeps/_workspace/src/github.com/gravitational/orbit/lib/utils/split.go +++ /dev/null @@ -1,16 +0,0 @@ -package utils - -// SplitAt splits array of strings at a given separator -func SplitAt(args []string, sep string) ([]string, []string) { - index := -1 - for i, a := range args { - if a == sep { - index = i - break - } - } - if index == -1 { - return args, []string{} - } - return args[:index], args[index+1:] -} diff --git a/Godeps/_workspace/src/github.com/gravitational/orbit/lib/utils/utils.go b/Godeps/_workspace/src/github.com/gravitational/orbit/lib/utils/utils.go deleted file mode 100644 index ab49adf896f..00000000000 --- a/Godeps/_workspace/src/github.com/gravitational/orbit/lib/utils/utils.go +++ /dev/null @@ -1,53 +0,0 @@ -package utils - -import ( - "bufio" - "strings" - "unicode/utf8" -) - -func SplitComma(v string) []string { - return Split(',', '\\', v) -} - -func Split(delim, escape rune, v string) []string { - if delim == 0 || escape == 0 { - return []string{v} - } - scanner := bufio.NewScanner(strings.NewReader(v)) - s := &splitter{delim: delim, escape: escape} - scanner.Split(s.scan) - - out := []string{} - for scanner.Scan() { - out = append(out, scanner.Text()) - } - return out -} - -type splitter struct { - delim rune - escape rune - prev rune -} - -func (s *splitter) scan(data []byte, atEOF bool) (advance int, token []byte, err error) { - // scan until first unescaped delimiter and return token - idx := 0 - var r rune - for width := 0; idx < len(data); idx += width { - r, width = utf8.DecodeRune(data[idx:]) - if r == s.delim && s.prev != s.escape && idx != 0 { - s.prev = r - return idx + width, data[:idx], nil - } - s.prev = r - } - - // If we're at EOF, we have a final, non-empty, non-terminated chunk - if atEOF && idx != 0 { - return len(data), data[:idx], nil - } - // Request more data. - return 0, nil, nil -} diff --git a/Godeps/_workspace/src/github.com/gravitational/orbit/lib/utils/utils_test.go b/Godeps/_workspace/src/github.com/gravitational/orbit/lib/utils/utils_test.go deleted file mode 100644 index 1f4efdaf697..00000000000 --- a/Godeps/_workspace/src/github.com/gravitational/orbit/lib/utils/utils_test.go +++ /dev/null @@ -1,36 +0,0 @@ -package utils - -import ( - "testing" - - . "github.com/gravitational/teleport/Godeps/_workspace/src/gopkg.in/check.v1" -) - -func TestUtils(t *testing.T) { TestingT(t) } - -type USuite struct { -} - -var _ = Suite(&USuite{}) - -func (s *USuite) TestSplit(c *C) { - tcs := []struct { - delim, escape rune - input string - expect []string - }{ - {delim: ',', escape: '\\', input: "", expect: []string{}}, - {delim: ',', escape: '\\', input: "a", expect: []string{"a"}}, - {delim: ',', escape: '\\', input: "a,b", expect: []string{"a", "b"}}, - {delim: ',', escape: '\\', input: "a,b\\,cd", expect: []string{"a", "b\\,cd"}}, - {delim: ',', escape: '\\', input: "a,b\\,cd,e", expect: []string{"a", "b\\,cd", "e"}}, - } - - for i, t := range tcs { - comment := Commentf( - "test case #%v: delim: %c, escape: %v, input: '%v', expected: %#v", - i, t.delim, t.escape, t.input, t.expect) - out := Split(t.delim, t.escape, t.input) - c.Assert(out, DeepEquals, t.expect, comment) - } -} diff --git a/Makefile b/Makefile index 84e7fa0751b..1d1897cd868 100644 --- a/Makefile +++ b/Makefile @@ -74,17 +74,7 @@ run-embedded: install run-telescope: install rm -f /tmp/telescope.auth.sock - telescope --log=console\ - --log-severity=INFO\ - --data-dir=/var/lib/teleport/telescope\ - --assets-dir=$(GOPATH)/src/github.com/gravitational/teleport/telescope\ - --cp-assets-dir=$(GOPATH)/src/github.com/gravitational/teleport\ - --fqdn=telescope.vendor.io\ - --domain=vendor.io\ - --tun-addr=tcp://0.0.0.0:34000\ - --web-addr=tcp://0.0.0.0:35000\ - --backend=bolt\ - --backend-config='{"path": "/var/lib/teleport/telescope.db"}' + telescope --config=examples/telescope.yaml trust-telescope: tscopectl user-ca pub-key > /tmp/user.pubkey diff --git a/assets/web/teleport/assets/static/tpl/login.tpl b/assets/web/teleport/assets/static/tpl/login.tpl index 9622ef24621..6a2db3195db 100644 --- a/assets/web/teleport/assets/static/tpl/login.tpl +++ b/assets/web/teleport/assets/static/tpl/login.tpl @@ -15,6 +15,9 @@
+
+ +
Forgot password? diff --git a/assets/web/telescope/assets/static/tpl/login.tpl b/assets/web/telescope/assets/static/tpl/login.tpl index 8d48374d416..22dd06a082f 100644 --- a/assets/web/telescope/assets/static/tpl/login.tpl +++ b/assets/web/telescope/assets/static/tpl/login.tpl @@ -15,6 +15,10 @@
+
+ +
+ Forgot password? diff --git a/examples/embedded.yaml b/examples/embedded.yaml index bb087558b42..1189f43ce34 100644 --- a/examples/embedded.yaml +++ b/examples/embedded.yaml @@ -8,6 +8,10 @@ fqdn: gravitational.io ssh: enabled: true +tun: + enabled: true + server_addr: tcp://telescope.vendor.io:33006 + auth: enabled: true domain: a.fqdn diff --git a/examples/telescope.yaml b/examples/telescope.yaml new file mode 100644 index 00000000000..05a9f437805 --- /dev/null +++ b/examples/telescope.yaml @@ -0,0 +1,22 @@ +log: + output: console + severity: INFO + +data_dir: /var/lib/telescope +fqdn: gravitational.io + +assets_dir: assets/web/telescope +cp_assets_dir: assets/web/teleport + +fqdn: telescope.vendor.io +domain: vendor.io + +backend: bolt\ +backend_config: '{"path": "/var/lib/teleport/telescope.db"}' + +keys_backend: + type: bolt + params: {path: "/var/lib/teleport/telescope.auth.db"} + + + diff --git a/lib/auth/auth.go b/lib/auth/auth.go index 7e8668b6e47..7774d98630d 100644 --- a/lib/auth/auth.go +++ b/lib/auth/auth.go @@ -132,7 +132,7 @@ func (s *AuthServer) GenerateUserCert( } func (s *AuthServer) SignIn(user string, password []byte) (*Session, error) { - if err := s.CheckPassword(user, password); err != nil { + if err := s.CheckPasswordWOToken(user, password); err != nil { return nil, err } sess, err := s.NewWebSession(user) diff --git a/lib/auth/auth_test.go b/lib/auth/auth_test.go index 461b6f10f86..a653ba02d6d 100644 --- a/lib/auth/auth_test.go +++ b/lib/auth/auth_test.go @@ -10,6 +10,8 @@ import ( "github.com/gravitational/teleport/lib/backend/encryptedbk" "github.com/gravitational/teleport/lib/backend/encryptedbk/encryptor" + "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/gokyle/hotp" + "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/gravitational/log" . "github.com/gravitational/teleport/Godeps/_workspace/src/gopkg.in/check.v1" @@ -56,10 +58,15 @@ func (s *AuthSuite) TestSessions(c *C) { pass := []byte("abc123") ws, err := s.a.SignIn(user, pass) - c.Assert(err, FitsTypeOf, &teleport.NotFoundError{}) + c.Assert(err, NotNil) c.Assert(ws, IsNil) - c.Assert(s.a.UpsertPassword(user, pass), IsNil) + hotpURL, _, err := s.a.UpsertPassword(user, pass) + c.Assert(err, IsNil) + otp, label, err := hotp.FromURL(hotpURL) + c.Assert(err, IsNil) + c.Assert(label, Equals, "user1") + otp.Increment() ws, err = s.a.SignIn(user, pass) c.Assert(err, IsNil) diff --git a/lib/auth/clt.go b/lib/auth/clt.go index 46c87404f68..49a39fe99df 100644 --- a/lib/auth/clt.go +++ b/lib/auth/clt.go @@ -331,19 +331,28 @@ func (c *Client) DeleteWebTun(prefix string) error { } // UpsertPassword updates web access password for the user -func (c *Client) UpsertPassword(user string, password []byte) error { - _, err := c.PostForm( +func (c *Client) UpsertPassword(user string, + password []byte) (hotpURL string, hotpQR []byte, err error) { + out, err := c.PostForm( c.Endpoint("users", user, "web", "password"), url.Values{"password": []string{string(password)}}, ) - return err + var re *upsertPasswordResponse + if err := json.Unmarshal(out.Bytes(), &re); err != nil { + return "", nil, err + } + return re.HotpURL, re.HotpQR, err } // CheckPassword checks if the suplied web access password is valid. -func (c *Client) CheckPassword(user string, password []byte) error { +func (c *Client) CheckPassword(user string, + password []byte, hotpToken string) error { _, err := c.PostForm( c.Endpoint("users", user, "web", "password", "check"), - url.Values{"password": []string{string(password)}}) + url.Values{ + "password": []string{string(password)}, + "hotpToken": []string{hotpToken}, + }) return err } @@ -352,7 +361,9 @@ func (c *Client) CheckPassword(user string, password []byte) error { func (c *Client) SignIn(user string, password []byte) (string, error) { out, err := c.PostForm( c.Endpoint("users", user, "web", "signin"), - url.Values{"password": []string{string(password)}}, + url.Values{ + "password": []string{string(password)}, + }, ) if err != nil { return "", err @@ -709,8 +720,8 @@ type ClientI interface { GetWebTuns() ([]services.WebTun, error) GetWebTun(prefix string) (*services.WebTun, error) DeleteWebTun(prefix string) error - UpsertPassword(user string, password []byte) error - CheckPassword(user string, password []byte) error + UpsertPassword(user string, password []byte) (hotpURL string, hotpQR []byte, err error) + CheckPassword(user string, password []byte, hotpToken string) error SignIn(user string, password []byte) (string, error) GetWebSession(user string, sid string) (string, error) GetWebSessionsKeys(user string) ([]services.AuthorizedKey, error) diff --git a/lib/auth/srv.go b/lib/auth/srv.go index 328ca859631..0a5320c54f2 100644 --- a/lib/auth/srv.go +++ b/lib/auth/srv.go @@ -312,7 +312,8 @@ func (s *APIServer) signIn(w http.ResponseWriter, r *http.Request, p httprouter. var pass string err := form.Parse(r, - form.String("password", &pass, form.Required())) + form.String("password", &pass, form.Required()), + ) if err != nil { replyErr(w, err) return @@ -336,25 +337,30 @@ func (s *APIServer) upsertPassword(w http.ResponseWriter, r *http.Request, p htt return } user := p[0].Value - if err := s.s.UpsertPassword(user, []byte(pass)); err != nil { + hotpURL, hotpQR, err := s.s.UpsertPassword(user, []byte(pass)) + if err != nil { replyErr(w, err) return } - reply(w, http.StatusOK, message(fmt.Sprintf("'%v' user password updated successfully", user))) + reply(w, http.StatusOK, + &upsertPasswordResponse{HotpURL: hotpURL, HotpQR: hotpQR}) } func (s *APIServer) checkPassword(w http.ResponseWriter, r *http.Request, p httprouter.Params) { var pass string + var hotpToken string err := form.Parse(r, - form.String("password", &pass, form.Required())) + form.String("password", &pass, form.Required()), + form.String("hotpToken", &hotpToken, form.Required()), + ) if err != nil { replyErr(w, err) return } user := p[0].Value - if err := s.s.CheckPassword(user, []byte(pass)); err != nil { + if err := s.s.CheckPassword(user, []byte(pass), hotpToken); err != nil { replyErr(w, err) return } @@ -939,6 +945,11 @@ type sealKeysResponse struct { Keys []encryptor.Key } +type upsertPasswordResponse struct { + HotpURL string + HotpQR []byte +} + func message(msg string) map[string]interface{} { return map[string]interface{}{"message": msg} } diff --git a/lib/auth/srv_test.go b/lib/auth/srv_test.go index fdec58e9219..5e223114ebd 100644 --- a/lib/auth/srv_test.go +++ b/lib/auth/srv_test.go @@ -6,6 +6,8 @@ import ( "testing" "time" + "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/gokyle/hotp" + "github.com/gravitational/teleport" authority "github.com/gravitational/teleport/lib/auth/native" "github.com/gravitational/teleport/lib/backend/boltbk" @@ -208,12 +210,34 @@ func (s *APISuite) TestUserKeyCRUD(c *C) { func (s *APISuite) TestPasswordCRUD(c *C) { pass := []byte("abc123") - err := s.clt.CheckPassword("user1", pass) + err := s.clt.CheckPassword("user1", pass, "123456") c.Assert(err, NotNil) - c.Assert(s.clt.UpsertPassword("user1", pass), IsNil) - c.Assert(s.clt.CheckPassword("user1", pass), IsNil) - c.Assert(s.clt.CheckPassword("user1", []byte("abc123123")), NotNil) + hotpURL, _, err := s.clt.UpsertPassword("user1", pass) + c.Assert(err, IsNil) + + otp, label, err := hotp.FromURL(hotpURL) + c.Assert(err, IsNil) + c.Assert(label, Equals, "user1") + otp.Increment() + + token1 := otp.OTP() + c.Assert(s.clt.CheckPassword("user1", pass, "123456"), NotNil) + c.Assert(s.clt.CheckPassword("user1", pass, token1), IsNil) + c.Assert(s.clt.CheckPassword("user1", pass, token1), NotNil) + + token2 := otp.OTP() + c.Assert(s.clt.CheckPassword("user1", []byte("abc123123"), token2), NotNil) + c.Assert(s.clt.CheckPassword("user1", pass, "123456"), NotNil) + c.Assert(s.clt.CheckPassword("user1", pass, token2), IsNil) + c.Assert(s.clt.CheckPassword("user1", pass, token1), NotNil) + + token3 := otp.OTP() + token4 := otp.OTP() + c.Assert(s.clt.CheckPassword("user1", pass, token4), NotNil) + c.Assert(s.clt.CheckPassword("user1", pass, token3), IsNil) + c.Assert(s.clt.CheckPassword("user1", pass, "123456"), NotNil) + c.Assert(s.clt.CheckPassword("user1", pass, token4), IsNil) } func (s *APISuite) TestSessions(c *C) { @@ -226,7 +250,13 @@ func (s *APISuite) TestSessions(c *C) { c.Assert(err, NotNil) c.Assert(ws, Equals, "") - c.Assert(s.clt.UpsertPassword(user, pass), IsNil) + hotpURL, _, err := s.clt.UpsertPassword(user, pass) + c.Assert(err, IsNil) + + otp, label, err := hotp.FromURL(hotpURL) + c.Assert(err, IsNil) + c.Assert(label, Equals, "user1") + otp.Increment() ws, err = s.clt.SignIn(user, pass) c.Assert(err, IsNil) diff --git a/lib/auth/tun.go b/lib/auth/tun.go index fe5179e0c44..61b9f4682c8 100644 --- a/lib/auth/tun.go +++ b/lib/auth/tun.go @@ -390,7 +390,7 @@ func (s *TunServer) passwordAuth( log.Infof("got authentication attempt for user '%v' type '%v'", conn.User(), ab.Type) switch ab.Type { case "password": - if err := s.a.CheckPassword(conn.User(), ab.Pass); err != nil { + if err := s.a.CheckPassword(conn.User(), ab.Pass, ab.HotpToken); err != nil { log.Errorf("Password auth error: %v", err) return nil, err } @@ -436,9 +436,10 @@ func (s *TunServer) passwordAuth( // authBucket uses password to transport app-specific user name and // auth-type in addition to the password to support auth type authBucket struct { - User string `json:"user"` - Type string `json:"type"` - Pass []byte `json:"pass"` + User string `json:"user"` + Type string `json:"type"` + Pass []byte `json:"pass"` + HotpToken string `json:"hotpToken"` } func NewTokenAuth(fqdn, token string) ([]ssh.AuthMethod, error) { @@ -465,11 +466,12 @@ func NewWebSessionAuth(user string, session []byte) ([]ssh.AuthMethod, error) { return []ssh.AuthMethod{ssh.Password(string(data))}, nil } -func NewWebPasswordAuth(user string, password []byte) ([]ssh.AuthMethod, error) { +func NewWebPasswordAuth(user string, password []byte, hotpToken string) ([]ssh.AuthMethod, error) { data, err := json.Marshal(authBucket{ - Type: AuthWebPassword, - User: user, - Pass: password, + Type: AuthWebPassword, + User: user, + Pass: password, + HotpToken: hotpToken, }) if err != nil { return nil, err diff --git a/lib/auth/tun_test.go b/lib/auth/tun_test.go index 6a0e495972c..a1879cc9f6f 100644 --- a/lib/auth/tun_test.go +++ b/lib/auth/tun_test.go @@ -8,6 +8,8 @@ import ( "net/url" "path/filepath" + "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/gokyle/hotp" + authority "github.com/gravitational/teleport/lib/auth/testauthority" "github.com/gravitational/teleport/lib/backend/boltbk" "github.com/gravitational/teleport/lib/backend/encryptedbk" @@ -131,9 +133,15 @@ func (s *TunSuite) TestUnixServerClient(c *C) { user := "test" pass := []byte("pwd123") - s.a.UpsertPassword(user, pass) + hotpURL, _, err := s.a.UpsertPassword(user, pass) + c.Assert(err, IsNil) - authMethod, err := NewWebPasswordAuth(user, pass) + otp, label, err := hotp.FromURL(hotpURL) + c.Assert(err, IsNil) + c.Assert(label, Equals, "test") + otp.Increment() + + authMethod, err := NewWebPasswordAuth(user, pass, otp.OTP()) c.Assert(err, IsNil) clt, err := NewTunClient( @@ -152,9 +160,15 @@ func (s *TunSuite) TestSessions(c *C) { user := "ws-test" pass := []byte("ws-abc123") - c.Assert(s.a.UpsertPassword(user, pass), IsNil) + hotpURL, _, err := s.a.UpsertPassword(user, pass) + c.Assert(err, IsNil) - authMethod, err := NewWebPasswordAuth(user, pass) + otp, label, err := hotp.FromURL(hotpURL) + c.Assert(err, IsNil) + c.Assert(label, Equals, "ws-test") + otp.Increment() + + authMethod, err := NewWebPasswordAuth(user, pass, otp.OTP()) c.Assert(err, IsNil) clt, err := NewTunClient( @@ -162,7 +176,13 @@ func (s *TunSuite) TestSessions(c *C) { c.Assert(err, IsNil) defer clt.Close() - c.Assert(clt.UpsertPassword(user, pass), IsNil) + hotpURL, _, err = clt.UpsertPassword(user, pass) + c.Assert(err, IsNil) + + otp, label, err = hotp.FromURL(hotpURL) + c.Assert(err, IsNil) + c.Assert(label, Equals, "ws-test") + otp.Increment() ws, err := clt.SignIn(user, pass) c.Assert(err, IsNil) @@ -194,9 +214,15 @@ func (s *TunSuite) TestSessionsBadPassword(c *C) { user := "system-test" pass := []byte("system-abc123") - c.Assert(s.a.UpsertPassword(user, pass), IsNil) + hotpURL, _, err := s.a.UpsertPassword(user, pass) + c.Assert(err, IsNil) - authMethod, err := NewWebPasswordAuth(user, pass) + otp, label, err := hotp.FromURL(hotpURL) + c.Assert(err, IsNil) + c.Assert(label, Equals, "system-test") + otp.Increment() + + authMethod, err := NewWebPasswordAuth(user, pass, otp.OTP()) c.Assert(err, IsNil) clt, err := NewTunClient( @@ -211,4 +237,5 @@ func (s *TunSuite) TestSessionsBadPassword(c *C) { ws, err = clt.SignIn("not-exitsts", pass) c.Assert(err, NotNil) c.Assert(ws, Equals, "") + } diff --git a/lib/cp/auth.go b/lib/cp/auth.go index 9a7e2af0065..eaa6edc2936 100644 --- a/lib/cp/auth.go +++ b/lib/cp/auth.go @@ -106,7 +106,7 @@ type RequestHandler func(http.ResponseWriter, *http.Request, httprouter.Params, type AuthHandler interface { GetHost() string - Auth(user, pass string) (string, error) + Auth(user, pass string, hotpToken string) (string, error) ValidateSession(user, sid string) (Context, error) SetSession(w http.ResponseWriter, user, sid string) error ClearSession(w http.ResponseWriter) @@ -143,8 +143,8 @@ func CloseContext(key string, val interface{}) { } } -func (s *LocalAuth) Auth(user, pass string) (string, error) { - method, err := auth.NewWebPasswordAuth(user, []byte(pass)) +func (s *LocalAuth) Auth(user, pass string, hotpToken string) (string, error) { + method, err := auth.NewWebPasswordAuth(user, []byte(pass), hotpToken) if err != nil { return "", err } diff --git a/lib/cp/cp.go b/lib/cp/cp.go index 26b3df0763f..13f0d3fa44e 100644 --- a/lib/cp/cp.go +++ b/lib/cp/cp.go @@ -635,17 +635,19 @@ func (s *CPHandler) logout(w http.ResponseWriter, r *http.Request, _ httprouter. } func (s *CPHandler) authForm(w http.ResponseWriter, r *http.Request, p httprouter.Params) { - var user, pass string + var user, pass, hotpToken string err := form.Parse(r, form.String("username", &user, form.Required()), - form.String("password", &pass, form.Required())) + form.String("password", &pass, form.Required()), + form.String("hotpToken", &hotpToken, form.Required()), + ) if err != nil { replyErr(w, http.StatusBadRequest, err) return } - sid, err := s.cfg.Auth.Auth(user, pass) + sid, err := s.cfg.Auth.Auth(user, pass, hotpToken) if err != nil { log.Warningf("auth error: %v", err) http.Redirect(w, r, "/login", http.StatusFound) diff --git a/lib/service/cfg.go b/lib/service/cfg.go index 6f42324f151..6e37b7e8abb 100644 --- a/lib/service/cfg.go +++ b/lib/service/cfg.go @@ -5,7 +5,6 @@ import ( "strings" "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/gravitational/configure" - outils "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/gravitational/orbit/lib/utils" "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/gravitational/trace" "github.com/gravitational/teleport/lib/utils" ) @@ -110,7 +109,7 @@ type TunConfig struct { type NetAddrSlice []utils.NetAddr func (s *NetAddrSlice) Set(val string) error { - values := outils.SplitComma(val) + values := configure.SplitComma(val) out := make([]utils.NetAddr, len(values)) for i, v := range values { a, err := utils.ParseAddr(v) @@ -130,7 +129,7 @@ func (kv *KeyVal) Set(v string) error { if len(*kv) == 0 { *kv = make(map[string]string) } - for _, i := range outils.SplitComma(v) { + for _, i := range configure.SplitComma(v) { vals := strings.SplitN(i, ":", 2) if len(vals) != 2 { return trace.Errorf("extra options should be defined like KEY:VAL") diff --git a/lib/service/tpl.go b/lib/service/tpl.go index 18747728ac3..eb6ca06d7f1 100644 --- a/lib/service/tpl.go +++ b/lib/service/tpl.go @@ -8,7 +8,7 @@ import ( "strings" "text/template" - "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/gravitational/orbit/lib/utils" + "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/gravitational/configure" "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/gravitational/trace" ) @@ -60,7 +60,7 @@ func (c *ctx) Env(key string) (string, error) { if !ok { return "", trace.Errorf("environment variable '%v' is not set", key) } - values := utils.SplitComma(v) + values := configure.SplitComma(v) out := make([]string, len(values)) for i, p := range values { out[i] = quoteYAML(p) diff --git a/lib/services/services_test.go b/lib/services/services_test.go index 38ec6cd1870..1d3d037e63a 100644 --- a/lib/services/services_test.go +++ b/lib/services/services_test.go @@ -60,6 +60,14 @@ func (s *BoltSuite) TestPasswordHashCRUD(c *C) { s.suite.PasswordHashCRUD(c) } +func (s *BoltSuite) TestPasswordAndHotpCRUD(c *C) { + s.suite.PasswordCRUD(c) +} + +func (s *BoltSuite) TestPasswordGarbage(c *C) { + s.suite.PasswordGarbage(c) +} + func (s *BoltSuite) TestWebSessionCRUD(c *C) { s.suite.WebSessionCRUD(c) } diff --git a/lib/services/test_suite.go b/lib/services/test_suite.go index e83ff9eaaa2..cb87b79f4f8 100644 --- a/lib/services/test_suite.go +++ b/lib/services/test_suite.go @@ -7,6 +7,8 @@ import ( "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/mailgun/lemma/random" "github.com/gravitational/teleport/lib/backend" + "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/gokyle/hotp" + . "github.com/gravitational/teleport/Godeps/_workspace/src/gopkg.in/check.v1" ) @@ -336,26 +338,48 @@ func (s *ServicesTestSuite) RemoteCertCRUD(c *C) { c.Assert(err, FitsTypeOf, &teleport.NotFoundError{}) } -func (s *ServicesTestSuite) TestPasswordCRUD(c *C) { +func (s *ServicesTestSuite) PasswordCRUD(c *C) { pass := []byte("abc123") - err := s.WebS.CheckPassword("user1", pass) - c.Assert(err, FitsTypeOf, &teleport.NotFoundError{}) + err := s.WebS.CheckPassword("user1", pass, "123456") + c.Assert(err, NotNil) - c.Assert(s.WebS.UpsertPassword("user1", pass), IsNil) - c.Assert(s.WebS.CheckPassword("user1", pass), IsNil) - c.Assert(s.WebS.CheckPassword("user1", []byte("abc123123")), FitsTypeOf, &teleport.BadParameterError{}) + hotpURL, _, err := s.WebS.UpsertPassword("user1", pass) + c.Assert(err, IsNil) + + otp, label, err := hotp.FromURL(hotpURL) + c.Assert(err, IsNil) + c.Assert(label, Equals, "user1") + otp.Increment() + + token1 := otp.OTP() + c.Assert(s.WebS.CheckPassword("user1", pass, "123456"), FitsTypeOf, &teleport.BadParameterError{}) + c.Assert(s.WebS.CheckPassword("user1", pass, token1), IsNil) + c.Assert(s.WebS.CheckPassword("user1", pass, token1), FitsTypeOf, &teleport.BadParameterError{}) + + token2 := otp.OTP() + c.Assert(s.WebS.CheckPassword("user1", []byte("abc123123"), token2), FitsTypeOf, &teleport.BadParameterError{}) + c.Assert(s.WebS.CheckPassword("user1", pass, "123456"), FitsTypeOf, &teleport.BadParameterError{}) + c.Assert(s.WebS.CheckPassword("user1", pass, token2), IsNil) + c.Assert(s.WebS.CheckPassword("user1", pass, token1), FitsTypeOf, &teleport.BadParameterError{}) + + token3 := otp.OTP() + token4 := otp.OTP() + c.Assert(s.WebS.CheckPassword("user1", pass, token4), FitsTypeOf, &teleport.BadParameterError{}) + c.Assert(s.WebS.CheckPassword("user1", pass, token3), IsNil) + c.Assert(s.WebS.CheckPassword("user1", pass, "123456"), FitsTypeOf, &teleport.BadParameterError{}) + c.Assert(s.WebS.CheckPassword("user1", pass, token4), IsNil) } -func (s *ServicesTestSuite) TestPasswordGarbage(c *C) { +func (s *ServicesTestSuite) PasswordGarbage(c *C) { garbage := [][]byte{ nil, make([]byte, MaxPasswordLength+1), make([]byte, MinPasswordLength-1), } for _, g := range garbage { - err := s.WebS.CheckPassword("user1", g) - c.Assert(err, FitsTypeOf, &teleport.BadParameterError{}) + err := s.WebS.CheckPassword("user1", g, "123456") + c.Assert(err, NotNil) } } diff --git a/lib/services/web.go b/lib/services/web.go index 69f66accbde..5a133a1421e 100644 --- a/lib/services/web.go +++ b/lib/services/web.go @@ -6,6 +6,8 @@ import ( "net/url" "time" + "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/gokyle/hotp" + "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/gravitational/log" "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/gravitational/trace" "github.com/gravitational/teleport/Godeps/_workspace/src/golang.org/x/crypto/bcrypt" @@ -42,6 +44,32 @@ func (s *WebService) GetPasswordHash(user string) ([]byte, error) { return hash, err } +func (s *WebService) UpsertHOTP(user string, otp *hotp.HOTP) error { + bytes, err := hotp.Marshal(otp) + if err != nil { + return trace.Wrap(err) + } + err = s.backend.UpsertVal([]string{"web", "users", user}, + "hotp", bytes, 0) + if err != nil { + return trace.Wrap(err) + } + return nil +} + +func (s *WebService) GetHOTP(user string) (*hotp.HOTP, error) { + bytes, err := s.backend.GetVal([]string{"web", "users", user}, + "hotp") + if err != nil { + return nil, trace.Wrap(err) + } + otp, err := hotp.Unmarshal(bytes) + if err != nil { + return nil, trace.Wrap(err) + } + return otp, nil +} + // UpsertSession func (s *WebService) UpsertWebSession(user, sid string, session WebSession, ttl time.Duration) error { @@ -174,28 +202,84 @@ func (s *WebService) GetWebTuns() ([]WebTun, error) { return tuns, nil } -func (s *WebService) UpsertPassword(user string, password []byte) error { +func (s *WebService) UpsertPassword(user string, + password []byte) (hotpURL string, hotpQR []byte, err error) { + if err := verifyPassword(password); err != nil { - return err + return "", nil, err } hash, err := bcrypt.GenerateFromPassword(password, bcrypt.DefaultCost) if err != nil { - return err + return "", nil, err } - return s.UpsertPasswordHash(user, hash) + + otp, err := hotp.GenerateHOTP(6, false) + if err != nil { + return "", nil, err + } + hotpQR, err = otp.QR(user) + if err != nil { + return "", nil, err + } + hotpURL = otp.URL(user) + if err != nil { + return "", nil, err + } + otp.Increment() + + err = s.UpsertPasswordHash(user, hash) + if err != nil { + return "", nil, err + } + err = s.UpsertHOTP(user, otp) + if err != nil { + return "", nil, err + } + + return hotpURL, hotpQR, nil + } -func (s *WebService) CheckPassword(user string, password []byte) error { +func (s *WebService) CheckPassword(user string, password []byte, hotpToken string) error { if err := verifyPassword(password); err != nil { - return err + return trace.Wrap(err) } hash, err := s.GetPasswordHash(user) if err != nil { - return err + return trace.Wrap(err) } if err := bcrypt.CompareHashAndPassword(hash, password); err != nil { return &teleport.BadParameterError{Err: "passwords do not match"} } + + otp, err := s.GetHOTP(user) + if err != nil { + return trace.Wrap(err) + } + if !otp.Check(hotpToken) { + return &teleport.BadParameterError{Err: "tokens do not match"} + } + + if err := s.UpsertHOTP(user, otp); err != nil { + return trace.Wrap(err) + } + + return nil +} + +// TO DO: not very good +func (s *WebService) CheckPasswordWOToken(user string, password []byte) error { + if err := verifyPassword(password); err != nil { + return trace.Wrap(err) + } + hash, err := s.GetPasswordHash(user) + if err != nil { + return trace.Wrap(err) + } + if err := bcrypt.CompareHashAndPassword(hash, password); err != nil { + return &teleport.BadParameterError{Err: "passwords do not match"} + } + return nil } diff --git a/tool/tctl/command/user.go b/tool/tctl/command/user.go index 88a7b3779ab..a7bd6b28985 100644 --- a/tool/tctl/command/user.go +++ b/tool/tctl/command/user.go @@ -2,6 +2,7 @@ package command import ( "fmt" + "io/ioutil" "time" "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/buger/goterm" @@ -9,12 +10,14 @@ import ( ) func (cmd *Command) SetPass(user, pass string) { - err := cmd.client.UpsertPassword(user, []byte(pass)) + hotpURL, hotpQR, err := cmd.client.UpsertPassword(user, []byte(pass)) if err != nil { cmd.printError(err) return } - cmd.printOK("password has been set for user '%v'", user) + + err = ioutil.WriteFile("QR.png", hotpQR, 0777) + cmd.printOK("password has been set for user '%v', hotp: %v", user, hotpURL) } func (cmd *Command) UpsertKey(user, keyID, key string, ttl time.Duration) { diff --git a/tool/telescope/telescope/srv/srv.go b/tool/telescope/telescope/srv/srv.go index 471b5e9178e..8edc89acf2e 100644 --- a/tool/telescope/telescope/srv/srv.go +++ b/tool/telescope/telescope/srv/srv.go @@ -94,17 +94,19 @@ func (h *Handler) logout(w http.ResponseWriter, r *http.Request, _ httprouter.Pa } func (h *Handler) authForm(w http.ResponseWriter, r *http.Request, p httprouter.Params) { - var user, pass string + var user, pass, hotpToken string err := form.Parse(r, form.String("username", &user, form.Required()), - form.String("password", &pass, form.Required())) + form.String("password", &pass, form.Required()), + form.String("hotpToken", &hotpToken, form.Required()), + ) if err != nil { replyErr(w, http.StatusBadRequest, err) return } - sid, err := h.auth.Auth(user, pass) + sid, err := h.auth.Auth(user, pass, hotpToken) if err != nil { log.Warningf("auth error: %v", err) http.Redirect(w, r, "/web/login", http.StatusFound)