From f9a3a4b3949dddecae413b97904c895b2cd887bf Mon Sep 17 00:00:00 2001 From: JoramWilander Date: Wed, 30 Mar 2016 12:49:29 -0400 Subject: Add MFA functionality --- Godeps/Godeps.json | 12 + .../src/github.com/dgryski/dgoogauth/.travis.yml | 1 + .../src/github.com/dgryski/dgoogauth/README.md | 15 + .../src/github.com/dgryski/dgoogauth/googauth.go | 199 ++++ .../github.com/dgryski/dgoogauth/googauth_test.go | 251 +++++ .../src/github.com/mattermost/rsc/gf256/Makefile | 8 + .../github.com/mattermost/rsc/gf256/blog_test.go | 85 ++ .../src/github.com/mattermost/rsc/gf256/gf256.go | 241 +++++ .../github.com/mattermost/rsc/gf256/gf256_test.go | 194 ++++ .../src/github.com/mattermost/rsc/qr/Makefile | 4 + .../github.com/mattermost/rsc/qr/coding/Makefile | 7 + .../src/github.com/mattermost/rsc/qr/coding/gen.go | 149 +++ .../src/github.com/mattermost/rsc/qr/coding/qr.go | 815 ++++++++++++++ .../github.com/mattermost/rsc/qr/coding/qr_test.go | 133 +++ .../mattermost/rsc/qr/libqrencode/Makefile | 4 + .../mattermost/rsc/qr/libqrencode/qrencode.go | 149 +++ .../src/github.com/mattermost/rsc/qr/png.go | 400 +++++++ .../src/github.com/mattermost/rsc/qr/png_test.go | 73 ++ .../src/github.com/mattermost/rsc/qr/qr.go | 116 ++ .../src/github.com/mattermost/rsc/qr/web/pic.go | 506 +++++++++ .../src/github.com/mattermost/rsc/qr/web/play.go | 1118 ++++++++++++++++++++ .../mattermost/rsc/qr/web/resize/resize.go | 152 +++ api/user.go | 197 +++- api/user_test.go | 76 ++ config/config.json | 1 + einterfaces/mfa.go | 25 + i18n/en.json | 60 ++ mattermost.go | 2 + model/client.go | 36 + model/config.go | 6 + model/license.go | 6 + model/user.go | 28 +- store/sql_user_store.go | 50 + store/sql_user_store_test.go | 44 + store/store.go | 2 + utils/config.go | 1 + utils/license.go | 1 + .../components/admin_console/service_settings.jsx | 55 + webapp/components/login.jsx | 307 ------ webapp/components/login/components/login_email.jsx | 121 +++ webapp/components/login/components/login_ldap.jsx | 101 ++ webapp/components/login/components/login_mfa.jsx | 92 ++ .../components/login/components/login_username.jsx | 121 +++ webapp/components/login/login.jsx | 419 ++++++++ webapp/components/login_email.jsx | 167 --- webapp/components/login_ldap.jsx | 145 --- webapp/components/login_username.jsx | 184 ---- webapp/components/signup_user_complete.jsx | 2 +- .../user_settings/user_settings_security.jsx | 223 +++- webapp/i18n/en.json | 6 + webapp/root.jsx | 3 +- webapp/utils/client.jsx | 38 +- webapp/utils/constants.jsx | 3 +- 53 files changed, 6319 insertions(+), 835 deletions(-) create mode 100644 Godeps/_workspace/src/github.com/dgryski/dgoogauth/.travis.yml create mode 100644 Godeps/_workspace/src/github.com/dgryski/dgoogauth/README.md create mode 100644 Godeps/_workspace/src/github.com/dgryski/dgoogauth/googauth.go create mode 100644 Godeps/_workspace/src/github.com/dgryski/dgoogauth/googauth_test.go create mode 100644 Godeps/_workspace/src/github.com/mattermost/rsc/gf256/Makefile create mode 100644 Godeps/_workspace/src/github.com/mattermost/rsc/gf256/blog_test.go create mode 100644 Godeps/_workspace/src/github.com/mattermost/rsc/gf256/gf256.go create mode 100644 Godeps/_workspace/src/github.com/mattermost/rsc/gf256/gf256_test.go create mode 100644 Godeps/_workspace/src/github.com/mattermost/rsc/qr/Makefile create mode 100644 Godeps/_workspace/src/github.com/mattermost/rsc/qr/coding/Makefile create mode 100644 Godeps/_workspace/src/github.com/mattermost/rsc/qr/coding/gen.go create mode 100644 Godeps/_workspace/src/github.com/mattermost/rsc/qr/coding/qr.go create mode 100644 Godeps/_workspace/src/github.com/mattermost/rsc/qr/coding/qr_test.go create mode 100644 Godeps/_workspace/src/github.com/mattermost/rsc/qr/libqrencode/Makefile create mode 100644 Godeps/_workspace/src/github.com/mattermost/rsc/qr/libqrencode/qrencode.go create mode 100644 Godeps/_workspace/src/github.com/mattermost/rsc/qr/png.go create mode 100644 Godeps/_workspace/src/github.com/mattermost/rsc/qr/png_test.go create mode 100644 Godeps/_workspace/src/github.com/mattermost/rsc/qr/qr.go create mode 100644 Godeps/_workspace/src/github.com/mattermost/rsc/qr/web/pic.go create mode 100644 Godeps/_workspace/src/github.com/mattermost/rsc/qr/web/play.go create mode 100644 Godeps/_workspace/src/github.com/mattermost/rsc/qr/web/resize/resize.go create mode 100644 einterfaces/mfa.go delete mode 100644 webapp/components/login.jsx create mode 100644 webapp/components/login/components/login_email.jsx create mode 100644 webapp/components/login/components/login_ldap.jsx create mode 100644 webapp/components/login/components/login_mfa.jsx create mode 100644 webapp/components/login/components/login_username.jsx create mode 100644 webapp/components/login/login.jsx delete mode 100644 webapp/components/login_email.jsx delete mode 100644 webapp/components/login_ldap.jsx delete mode 100644 webapp/components/login_username.jsx diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index f94cafc1c..aff57a2af 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -22,6 +22,10 @@ "ImportPath": "github.com/cloudfoundry/jibber_jabber", "Rev": "bcc4c8345a21301bf47c032ff42dd1aae2fe3027" }, + { + "ImportPath": "github.com/dgryski/dgoogauth", + "Rev": "67642ac6f9144f6610279e37e7be9af13f1cd668" + }, { "ImportPath": "github.com/disintegration/imaging", "Rev": "546cb3c5137b3f1232e123a26aa033aade6b3066" @@ -98,6 +102,14 @@ "Comment": "go1.0-cutoff-63-g11fc39a", "Rev": "11fc39a580a008f1f39bb3d11d984fb34ed778d9" }, + { + "ImportPath": "github.com/mattermost/rsc/gf256", + "Rev": "bbaefb05eaa0389ea712340066837c8ce4d287f9" + }, + { + "ImportPath": "github.com/mattermost/rsc/qr", + "Rev": "bbaefb05eaa0389ea712340066837c8ce4d287f9" + }, { "ImportPath": "github.com/mssola/user_agent", "Comment": "v0.4.1-5-g783ec61", diff --git a/Godeps/_workspace/src/github.com/dgryski/dgoogauth/.travis.yml b/Godeps/_workspace/src/github.com/dgryski/dgoogauth/.travis.yml new file mode 100644 index 000000000..4f2ee4d97 --- /dev/null +++ b/Godeps/_workspace/src/github.com/dgryski/dgoogauth/.travis.yml @@ -0,0 +1 @@ +language: go diff --git a/Godeps/_workspace/src/github.com/dgryski/dgoogauth/README.md b/Godeps/_workspace/src/github.com/dgryski/dgoogauth/README.md new file mode 100644 index 000000000..75fdde78a --- /dev/null +++ b/Godeps/_workspace/src/github.com/dgryski/dgoogauth/README.md @@ -0,0 +1,15 @@ +This is a Go implementation of the Google Authenticator library. + +[![Build Status](https://travis-ci.org/dgryski/dgoogauth.png)](https://travis-ci.org/dgryski/dgoogauth) + +Copyright (c) 2012 Damian Gryski +This code is licensed under the Apache License, version 2.0 + +It implements the one-time-password algorithms specified in: + +* RFC 4226 (HOTP: An HMAC-Based One-Time Password Algorithm) +* RFC 6238 (TOTP: Time-Based One-Time Password Algorithm) + +You can learn more about the Google Authenticator library at its project page: + +* https://github.com/google/google-authenticator diff --git a/Godeps/_workspace/src/github.com/dgryski/dgoogauth/googauth.go b/Godeps/_workspace/src/github.com/dgryski/dgoogauth/googauth.go new file mode 100644 index 000000000..1efddcc20 --- /dev/null +++ b/Godeps/_workspace/src/github.com/dgryski/dgoogauth/googauth.go @@ -0,0 +1,199 @@ +/* +Package dgoogauth implements the one-time password algorithms supported by Google Authenticator + +This package supports the HMAC-Based One-time Password (HOTP) algorithm +specified in RFC 4226 and the Time-based One-time Password (TOTP) algorithm +specified in RFC 6238. +*/ +package dgoogauth + +import ( + "crypto/hmac" + "crypto/sha1" + "encoding/base32" + "encoding/binary" + "errors" + "net/url" + "sort" + "strconv" + "time" +) + +// Much of this code assumes int == int64, which probably is not the case. + +// ComputeCode computes the response code for a 64-bit challenge 'value' using the secret 'secret'. +// To avoid breaking compatibility with the previous API, it returns an invalid code (-1) when an error occurs, +// but does not silently ignore them (it forces a mismatch so the code will be rejected). +func ComputeCode(secret string, value int64) int { + + key, err := base32.StdEncoding.DecodeString(secret) + if err != nil { + return -1 + } + + hash := hmac.New(sha1.New, key) + err = binary.Write(hash, binary.BigEndian, value) + if err != nil { + return -1 + } + h := hash.Sum(nil) + + offset := h[19] & 0x0f + + truncated := binary.BigEndian.Uint32(h[offset : offset+4]) + + truncated &= 0x7fffffff + code := truncated % 1000000 + + return int(code) +} + +// ErrInvalidCode indicate the supplied one-time code was not valid +var ErrInvalidCode = errors.New("invalid code") + +// OTPConfig is a one-time-password configuration. This object will be modified by calls to +// Authenticate and should be saved to ensure the codes are in fact only used +// once. +type OTPConfig struct { + Secret string // 80-bit base32 encoded string of the user's secret + WindowSize int // valid range: technically 0..100 or so, but beyond 3-5 is probably bad security + HotpCounter int // the current otp counter. 0 if the user uses time-based codes instead. + DisallowReuse []int // timestamps in the current window unavailable for re-use + ScratchCodes []int // an array of 8-digit numeric codes that can be used to log in + UTC bool // use UTC for the timestamp instead of local time +} + +func (c *OTPConfig) checkScratchCodes(code int) bool { + + for i, v := range c.ScratchCodes { + if code == v { + // remove this code from the list of valid ones + l := len(c.ScratchCodes) - 1 + c.ScratchCodes[i] = c.ScratchCodes[l] // copy last element over this element + c.ScratchCodes = c.ScratchCodes[0:l] // and trim the list length by 1 + return true + } + } + + return false +} + +func (c *OTPConfig) checkHotpCode(code int) bool { + + for i := 0; i < c.WindowSize; i++ { + if ComputeCode(c.Secret, int64(c.HotpCounter+i)) == code { + c.HotpCounter += i + 1 + // We don't check for overflow here, which means you can only authenticate 2^63 times + // After that, the counter is negative and the above 'if' test will fail. + // This matches the behaviour of the PAM module. + return true + } + } + + // we must always advance the counter if we tried to authenticate with it + c.HotpCounter++ + return false +} + +func (c *OTPConfig) checkTotpCode(t0, code int) bool { + + minT := t0 - (c.WindowSize / 2) + maxT := t0 + (c.WindowSize / 2) + for t := minT; t <= maxT; t++ { + if ComputeCode(c.Secret, int64(t)) == code { + + if c.DisallowReuse != nil { + for _, timeCode := range c.DisallowReuse { + if timeCode == t { + return false + } + } + + // code hasn't been used before + c.DisallowReuse = append(c.DisallowReuse, t) + + // remove all time codes outside of the valid window + sort.Ints(c.DisallowReuse) + min := 0 + for c.DisallowReuse[min] < minT { + min++ + } + // FIXME: check we don't have an off-by-one error here + c.DisallowReuse = c.DisallowReuse[min:] + } + + return true + } + } + + return false +} + +// Authenticate a one-time-password against the given OTPConfig +// Returns true/false if the authentication was successful. +// Returns error if the password is incorrectly formatted (not a zero-padded 6 or non-zero-padded 8 digit number). +func (c *OTPConfig) Authenticate(password string) (bool, error) { + + var scratch bool + + switch { + case len(password) == 6 && password[0] >= '0' && password[0] <= '9': + break + case len(password) == 8 && password[0] >= '1' && password[0] <= '9': + scratch = true + break + default: + return false, ErrInvalidCode + } + + code, err := strconv.Atoi(password) + + if err != nil { + return false, ErrInvalidCode + } + + if scratch { + return c.checkScratchCodes(code), nil + } + + // we have a counter value we can use + if c.HotpCounter > 0 { + return c.checkHotpCode(code), nil + } + + var t0 int + // assume we're on Time-based OTP + if c.UTC { + t0 = int(time.Now().UTC().Unix() / 30) + } else { + t0 = int(time.Now().Unix() / 30) + } + return c.checkTotpCode(t0, code), nil +} + +// ProvisionURI generates a URI that can be turned into a QR code to configure +// a Google Authenticator mobile app. +func (c *OTPConfig) ProvisionURI(user string) string { + return c.ProvisionURIWithIssuer(user, "") +} + +// ProvisionURIWithIssuer generates a URI that can be turned into a QR code +// to configure a Google Authenticator mobile app. It respects the recommendations +// on how to avoid conflicting accounts. +// +// See https://code.google.com/p/google-authenticator/wiki/ConflictingAccounts +func (c *OTPConfig) ProvisionURIWithIssuer(user string, issuer string) string { + auth := "totp/" + q := make(url.Values) + if c.HotpCounter > 0 { + auth = "hotp/" + q.Add("counter", strconv.Itoa(c.HotpCounter)) + } + q.Add("secret", c.Secret) + if issuer != "" { + q.Add("issuer", issuer) + auth += issuer + ":" + } + + return "otpauth://" + auth + user + "?" + q.Encode() +} diff --git a/Godeps/_workspace/src/github.com/dgryski/dgoogauth/googauth_test.go b/Godeps/_workspace/src/github.com/dgryski/dgoogauth/googauth_test.go new file mode 100644 index 000000000..031922c47 --- /dev/null +++ b/Godeps/_workspace/src/github.com/dgryski/dgoogauth/googauth_test.go @@ -0,0 +1,251 @@ +package dgoogauth + +import ( + "strconv" + "testing" + "time" +) + +// Test vectors via: +// http://code.google.com/p/google-authenticator/source/browse/libpam/pam_google_authenticator_unittest.c +// https://google-authenticator.googlecode.com/hg/libpam/totp.html + +var codeTests = []struct { + secret string + value int64 + code int +}{ + {"2SH3V3GDW7ZNMGYE", 1, 293240}, + {"2SH3V3GDW7ZNMGYE", 5, 932068}, + {"2SH3V3GDW7ZNMGYE", 10000, 50548}, +} + +func TestCode(t *testing.T) { + + for _, v := range codeTests { + c := ComputeCode(v.secret, v.value) + + if c != v.code { + t.Errorf("computeCode(%s, %d): got %d expected %d\n", v.secret, v.value, c, v.code) + } + + } +} + +func TestScratchCode(t *testing.T) { + + var cotp OTPConfig + + cotp.ScratchCodes = []int{11112222, 22223333} + + var scratchTests = []struct { + code int + result bool + }{ + {33334444, false}, + {11112222, true}, + {11112222, false}, + {22223333, true}, + {22223333, false}, + {33334444, false}, + } + + for _, s := range scratchTests { + r := cotp.checkScratchCodes(s.code) + if r != s.result { + t.Errorf("scratchcode(%d) failed: got %t expected %t", s.code, r, s.result) + } + } +} + +func TestHotpCode(t *testing.T) { + + var cotp OTPConfig + + // reuse our test values from above + // perhaps create more? + cotp.Secret = "2SH3V3GDW7ZNMGYE" + cotp.HotpCounter = 1 + cotp.WindowSize = 3 + + var counterCodes = []struct { + code int + result bool + counter int + }{ + { /* 1 */ 293240, true, 2}, // increments on success + { /* 1 */ 293240, false, 3}, // and failure + { /* 5 */ 932068, true, 6}, // inside of window + { /* 10 */ 481725, false, 7}, // outside of window + { /* 10 */ 481725, false, 8}, // outside of window + { /* 10 */ 481725, true, 11}, // now inside of window + } + + for i, s := range counterCodes { + r := cotp.checkHotpCode(s.code) + if r != s.result { + t.Errorf("counterCode(%d) (step %d) failed: got %t expected %t", s.code, i, r, s.result) + } + if cotp.HotpCounter != s.counter { + t.Errorf("hotpCounter incremented poorly: got %d expected %d", cotp.HotpCounter, s.counter) + } + } +} + +func TestTotpCode(t *testing.T) { + + var cotp OTPConfig + + // reuse our test values from above + cotp.Secret = "2SH3V3GDW7ZNMGYE" + cotp.WindowSize = 5 + + var windowTest = []struct { + code int + t0 int + result bool + }{ + {50548, 9997, false}, + {50548, 9998, true}, + {50548, 9999, true}, + {50548, 10000, true}, + {50548, 10001, true}, + {50548, 10002, true}, + {50548, 10003, false}, + } + + for i, s := range windowTest { + r := cotp.checkTotpCode(s.t0, s.code) + if r != s.result { + t.Errorf("counterCode(%d) (step %d) failed: got %t expected %t", s.code, i, r, s.result) + } + } + + cotp.DisallowReuse = make([]int, 0) + var noreuseTest = []struct { + code int + t0 int + result bool + disallowed []int + }{ + {50548 /* 10000 */, 9997, false, []int{}}, + {50548 /* 10000 */, 9998, true, []int{10000}}, + {50548 /* 10000 */, 9999, false, []int{10000}}, + {478726 /* 10001 */, 10001, true, []int{10000, 10001}}, + {646986 /* 10002 */, 10002, true, []int{10000, 10001, 10002}}, + {842639 /* 10003 */, 10003, true, []int{10001, 10002, 10003}}, + } + + for i, s := range noreuseTest { + r := cotp.checkTotpCode(s.t0, s.code) + if r != s.result { + t.Errorf("timeCode(%d) (step %d) failed: got %t expected %t", s.code, i, r, s.result) + } + if len(cotp.DisallowReuse) != len(s.disallowed) { + t.Errorf("timeCode(%d) (step %d) failed: disallowReuse len mismatch: got %d expected %d", s.code, i, len(cotp.DisallowReuse), len(s.disallowed)) + } else { + same := true + for j := range s.disallowed { + if s.disallowed[j] != cotp.DisallowReuse[j] { + same = false + } + } + if !same { + t.Errorf("timeCode(%d) (step %d) failed: disallowReused: got %v expected %v", s.code, i, cotp.DisallowReuse, s.disallowed) + } + } + } +} + +func TestAuthenticate(t *testing.T) { + + otpconf := &OTPConfig{ + Secret: "2SH3V3GDW7ZNMGYE", + WindowSize: 3, + HotpCounter: 1, + ScratchCodes: []int{11112222, 22223333}, + } + + type attempt struct { + code string + result bool + } + + var attempts = []attempt{ + {"foobar", false}, // not digits + {"1fooba", false}, // not valid number + {"1111111", false}, // bad length + { /* 1 */ "293240", true}, // hopt increments on success + { /* 1 */ "293240", false}, // hopt failure + {"33334444", false}, // scratch + {"11112222", true}, + {"11112222", false}, + } + + for _, a := range attempts { + r, _ := otpconf.Authenticate(a.code) + if r != a.result { + t.Errorf("bad result from code=%s: got %t expected %t\n", a.code, r, a.result) + } + } + + // let's check some time-based codes + otpconf.HotpCounter = 0 + // I haven't mocked the clock, so we'll just compute one + var t0 int64 + if otpconf.UTC { + t0 = int64(time.Now().UTC().Unix() / 30) + } else { + t0 = int64(time.Now().Unix() / 30) + } + c := ComputeCode(otpconf.Secret, t0) + + invalid := c + 1 + attempts = []attempt{ + {strconv.Itoa(invalid), false}, + {strconv.Itoa(c), true}, + } + + for _, a := range attempts { + r, _ := otpconf.Authenticate(a.code) + if r != a.result { + t.Errorf("bad result from code=%s: got %t expected %t\n", a.code, r, a.result) + } + + otpconf.UTC = true + r, _ = otpconf.Authenticate(a.code) + if r != a.result { + t.Errorf("bad result from code=%s: got %t expected %t\n", a.code, r, a.result) + } + otpconf.UTC = false + } + +} + +func TestProvisionURI(t *testing.T) { + otpconf := OTPConfig{ + Secret: "x", + } + + cases := []struct { + user, iss string + hotp bool + out string + }{ + {"test", "", false, "otpauth://totp/test?secret=x"}, + {"test", "", true, "otpauth://hotp/test?counter=1&secret=x"}, + {"test", "Company", true, "otpauth://hotp/Company:test?counter=1&issuer=Company&secret=x"}, + {"test", "Company", false, "otpauth://totp/Company:test?issuer=Company&secret=x"}, + } + + for i, c := range cases { + otpconf.HotpCounter = 0 + if c.hotp { + otpconf.HotpCounter = 1 + } + got := otpconf.ProvisionURIWithIssuer(c.user, c.iss) + if got != c.out { + t.Errorf("%d: want %q, got %q", i, c.out, got) + } + } +} diff --git a/Godeps/_workspace/src/github.com/mattermost/rsc/gf256/Makefile b/Godeps/_workspace/src/github.com/mattermost/rsc/gf256/Makefile new file mode 100644 index 000000000..518a034f3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mattermost/rsc/gf256/Makefile @@ -0,0 +1,8 @@ +# 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. + +include $(GOROOT)/src/Make.inc +TARG=rsc.googlecode.com/hg/gf256 +GOFILES=gf256.go #rs.go +include $(GOROOT)/src/Make.pkg diff --git a/Godeps/_workspace/src/github.com/mattermost/rsc/gf256/blog_test.go b/Godeps/_workspace/src/github.com/mattermost/rsc/gf256/blog_test.go new file mode 100644 index 000000000..12cc7deb0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mattermost/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/github.com/mattermost/rsc/gf256/gf256.go b/Godeps/_workspace/src/github.com/mattermost/rsc/gf256/gf256.go new file mode 100644 index 000000000..34cc975a8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mattermost/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 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/github.com/mattermost/rsc/gf256/gf256_test.go b/Godeps/_workspace/src/github.com/mattermost/rsc/gf256/gf256_test.go new file mode 100644 index 000000000..f77fa7d67 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mattermost/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/github.com/mattermost/rsc/qr/coding/qr_test.go b/Godeps/_workspace/src/github.com/mattermost/rsc/qr/coding/qr_test.go new file mode 100644 index 000000000..b8199bb51 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mattermost/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/mattermost/rsc/gf256" + "github.com/mattermost/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/github.com/mattermost/rsc/qr/libqrencode/Makefile b/Godeps/_workspace/src/github.com/mattermost/rsc/qr/libqrencode/Makefile new file mode 100644 index 000000000..4c9591462 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mattermost/rsc/qr/libqrencode/Makefile @@ -0,0 +1,4 @@ +include $(GOROOT)/src/Make.inc +TARG=rsc.googlecode.com/hg/qr/libqrencode +CGOFILES=qrencode.go +include $(GOROOT)/src/Make.pkg diff --git a/Godeps/_workspace/src/github.com/mattermost/rsc/qr/libqrencode/qrencode.go b/Godeps/_workspace/src/github.com/mattermost/rsc/qr/libqrencode/qrencode.go new file mode 100644 index 000000000..f4ce3ffb6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mattermost/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/github.com/mattermost/rsc/qr/png.go b/Godeps/_workspace/src/github.com/mattermost/rsc/qr/png.go new file mode 100644 index 000000000..db49d0577 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mattermost/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/github.com/mattermost/rsc/qr/png_test.go b/Godeps/_workspace/src/github.com/mattermost/rsc/qr/png_test.go new file mode 100644 index 000000000..27a622924 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mattermost/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/github.com/mattermost/rsc/qr/qr.go b/Godeps/_workspace/src/github.com/mattermost/rsc/qr/qr.go new file mode 100644 index 000000000..1d20d02f3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mattermost/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/mattermost/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/github.com/mattermost/rsc/qr/web/play.go b/Godeps/_workspace/src/github.com/mattermost/rsc/qr/web/play.go new file mode 100644 index 000000000..120f50b81 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mattermost/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" + + "github.com/mattermost/rsc/appfs/fs" + "github.com/mattermost/rsc/gf256" + "github.com/mattermost/rsc/qr" + "github.com/mattermost/rsc/qr/coding" + "github.com/mattermost/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/github.com/mattermost/rsc/qr/web/resize/resize.go b/Godeps/_workspace/src/github.com/mattermost/rsc/qr/web/resize/resize.go new file mode 100644 index 000000000..02c8b0040 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mattermost/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/api/user.go b/api/user.go index 43969158a..60b92f90d 100644 --- a/api/user.go +++ b/api/user.go @@ -53,6 +53,9 @@ func InitUser(r *mux.Router) { sr.Handle("/attach_device", ApiUserRequired(attachDeviceId)).Methods("POST") sr.Handle("/verify_email", ApiAppHandler(verifyEmail)).Methods("POST") sr.Handle("/resend_verification", ApiAppHandler(resendVerification)).Methods("POST") + sr.Handle("/mfa", ApiAppHandler(checkMfa)).Methods("POST") + sr.Handle("/generate_mfa_qr", ApiUserRequired(generateMfaQrCode)).Methods("GET") + sr.Handle("/update_mfa", ApiUserRequired(updateMfa)).Methods("POST") sr.Handle("/newimage", ApiUserRequired(uploadProfileImage)).Methods("POST") @@ -405,13 +408,14 @@ func SendVerifyEmailAndForget(c *Context, userId, userEmail, teamName, teamDispl }() } -func LoginById(c *Context, w http.ResponseWriter, r *http.Request, userId, password, deviceId string) *model.User { +func LoginById(c *Context, w http.ResponseWriter, r *http.Request, userId, password, mfaToken, deviceId string) *model.User { if result := <-Srv.Store.User().Get(userId); result.Err != nil { c.Err = result.Err return nil } else { user := result.Data.(*model.User) - if checkUserLoginAttempts(c, user) && checkUserPassword(c, user, password) { + + if authenticateUserPasswordAndToken(c, user, password, mfaToken) { Login(c, w, r, user, deviceId) return user } @@ -420,7 +424,7 @@ func LoginById(c *Context, w http.ResponseWriter, r *http.Request, userId, passw return nil } -func LoginByEmail(c *Context, w http.ResponseWriter, r *http.Request, email, name, password, deviceId string) *model.User { +func LoginByEmail(c *Context, w http.ResponseWriter, r *http.Request, email, name, password, mfaToken, deviceId string) *model.User { var team *model.Team if result := <-Srv.Store.Team().GetByName(name); result.Err != nil { @@ -443,7 +447,7 @@ func LoginByEmail(c *Context, w http.ResponseWriter, r *http.Request, email, nam return nil } - if checkUserLoginAttempts(c, user) && checkUserPassword(c, user, password) { + if authenticateUserPasswordAndToken(c, user, password, mfaToken) { Login(c, w, r, user, deviceId) return user } @@ -452,7 +456,7 @@ func LoginByEmail(c *Context, w http.ResponseWriter, r *http.Request, email, nam return nil } -func LoginByUsername(c *Context, w http.ResponseWriter, r *http.Request, username, name, password, deviceId string) *model.User { +func LoginByUsername(c *Context, w http.ResponseWriter, r *http.Request, username, name, password, mfaToken, deviceId string) *model.User { var team *model.Team if result := <-Srv.Store.Team().GetByName(name); result.Err != nil { @@ -475,7 +479,7 @@ func LoginByUsername(c *Context, w http.ResponseWriter, r *http.Request, usernam return nil } - if checkUserLoginAttempts(c, user) && checkUserPassword(c, user, password) { + if authenticateUserPasswordAndToken(c, user, password, mfaToken) { Login(c, w, r, user, deviceId) return user } @@ -518,6 +522,10 @@ func LoginByOAuth(c *Context, w http.ResponseWriter, r *http.Request, service st } } +func authenticateUserPasswordAndToken(c *Context, user *model.User, password string, token string) bool { + return checkUserLoginAttempts(c, user) && checkUserMfa(c, user, token) && checkUserPassword(c, user, password) +} + func checkUserLoginAttempts(c *Context, user *model.User) bool { if user.FailedAttempts >= utils.Cfg.ServiceSettings.MaximumLoginAttempts { c.LogAuditWithUserId(user.Id, "fail") @@ -530,7 +538,6 @@ func checkUserLoginAttempts(c *Context, user *model.User) bool { } func checkUserPassword(c *Context, user *model.User, password string) bool { - if !model.ComparePassword(user.Password, password) { c.LogAuditWithUserId(user.Id, "fail") c.Err = model.NewLocAppError("checkUserPassword", "api.user.check_user_password.invalid.app_error", nil, "user_id="+user.Id) @@ -548,7 +555,29 @@ func checkUserPassword(c *Context, user *model.User, password string) bool { return true } +} + +func checkUserMfa(c *Context, user *model.User, token string) bool { + if !user.MfaActive || !utils.IsLicensed || !*utils.License.Features.MFA || !*utils.Cfg.ServiceSettings.EnableMultifactorAuthentication { + return true + } + mfaInterface := einterfaces.GetMfaInterface() + if mfaInterface == nil { + c.Err = model.NewLocAppError("checkUserMfa", "api.user.check_user_mfa.not_available.app_error", nil, "") + c.Err.StatusCode = http.StatusNotImplemented + return false + } + + if ok, err := mfaInterface.ValidateToken(user.MfaSecret, token); err != nil { + c.Err = err + return false + } else if !ok { + c.Err = model.NewLocAppError("checkUserMfa", "api.user.check_user_mfa.bad_code.app_error", nil, "") + return false + } else { + return true + } } // User MUST be validated before calling Login @@ -660,11 +689,11 @@ func login(c *Context, w http.ResponseWriter, r *http.Request) { var user *model.User if len(props["id"]) != 0 { - user = LoginById(c, w, r, props["id"], props["password"], props["device_id"]) + user = LoginById(c, w, r, props["id"], props["password"], props["token"], props["device_id"]) } else if len(props["email"]) != 0 && len(props["name"]) != 0 { - user = LoginByEmail(c, w, r, props["email"], props["name"], props["password"], props["device_id"]) + user = LoginByEmail(c, w, r, props["email"], props["name"], props["password"], props["token"], props["device_id"]) } else if len(props["username"]) != 0 && len(props["name"]) != 0 { - user = LoginByUsername(c, w, r, props["username"], props["name"], props["password"], props["device_id"]) + user = LoginByUsername(c, w, r, props["username"], props["name"], props["password"], props["token"], props["device_id"]) } else { c.Err = model.NewLocAppError("login", "api.user.login.not_provided.app_error", nil, "") c.Err.StatusCode = http.StatusForbidden @@ -695,6 +724,7 @@ func loginLdap(c *Context, w http.ResponseWriter, r *http.Request) { password := props["password"] id := props["id"] teamName := props["teamName"] + mfaToken := props["token"] if len(password) == 0 { c.Err = model.NewLocAppError("loginLdap", "api.user.login_ldap.blank_pwd.app_error", nil, "") @@ -735,6 +765,10 @@ func loginLdap(c *Context, w http.ResponseWriter, r *http.Request) { return } + if !checkUserMfa(c, user, mfaToken) { + return + } + // User is authenticated at this point Login(c, w, r, user, props["device_id"]) @@ -2487,3 +2521,146 @@ func resendVerification(c *Context, w http.ResponseWriter, r *http.Request) { } } } + +func generateMfaQrCode(c *Context, w http.ResponseWriter, r *http.Request) { + uchan := Srv.Store.User().Get(c.Session.UserId) + tchan := Srv.Store.Team().Get(c.Session.TeamId) + + var user *model.User + if result := <-uchan; result.Err != nil { + c.Err = result.Err + return + } else { + user = result.Data.(*model.User) + } + + var team *model.Team + if result := <-tchan; result.Err != nil { + c.Err = result.Err + return + } else { + team = result.Data.(*model.Team) + } + + mfaInterface := einterfaces.GetMfaInterface() + if mfaInterface == nil { + c.Err = model.NewLocAppError("generateMfaQrCode", "api.user.generate_mfa_qr.not_available.app_error", nil, "") + c.Err.StatusCode = http.StatusNotImplemented + return + } + + img, err := mfaInterface.GenerateQrCode(team, user) + if err != nil { + c.Err = err + return + } + + w.Header().Del("Content-Type") // Content-Type will be set automatically by the http writer + w.Write(img) +} + +func updateMfa(c *Context, w http.ResponseWriter, r *http.Request) { + props := model.StringInterfaceFromJson(r.Body) + + activate, ok := props["activate"].(bool) + if !ok { + c.SetInvalidParam("updateMfa", "activate") + return + } + + token := "" + if activate { + token = props["token"].(string) + if len(token) == 0 { + c.SetInvalidParam("updateMfa", "token") + return + } + } + + mfaInterface := einterfaces.GetMfaInterface() + if mfaInterface == nil { + c.Err = model.NewLocAppError("generateMfaQrCode", "api.user.update_mfa.not_available.app_error", nil, "") + c.Err.StatusCode = http.StatusNotImplemented + return + } + + if activate { + var user *model.User + if result := <-Srv.Store.User().Get(c.Session.UserId); result.Err != nil { + c.Err = result.Err + return + } else { + user = result.Data.(*model.User) + } + + if err := mfaInterface.Activate(user, token); err != nil { + c.Err = err + return + } + } else { + if err := mfaInterface.Deactivate(c.Session.UserId); err != nil { + c.Err = err + return + } + } + + rdata := map[string]string{} + rdata["status"] = "ok" + w.Write([]byte(model.MapToJson(rdata))) +} + +func checkMfa(c *Context, w http.ResponseWriter, r *http.Request) { + if !utils.IsLicensed || !*utils.License.Features.MFA || !*utils.Cfg.ServiceSettings.EnableMultifactorAuthentication { + rdata := map[string]string{} + rdata["mfa_required"] = "false" + w.Write([]byte(model.MapToJson(rdata))) + return + } + + props := model.MapFromJson(r.Body) + + method := props["method"] + if method != model.USER_AUTH_SERVICE_EMAIL && + method != model.USER_AUTH_SERVICE_USERNAME && + method != model.USER_AUTH_SERVICE_LDAP { + c.SetInvalidParam("checkMfa", "method") + return + } + + teamName := props["team_name"] + if len(teamName) == 0 { + c.SetInvalidParam("checkMfa", "team_name") + return + } + + loginId := props["login_id"] + if len(loginId) == 0 { + c.SetInvalidParam("checkMfa", "login_id") + return + } + + var team *model.Team + if result := <-Srv.Store.Team().GetByName(teamName); result.Err != nil { + c.Err = result.Err + return + } else { + team = result.Data.(*model.Team) + } + + var uchan store.StoreChannel + if method == model.USER_AUTH_SERVICE_EMAIL { + uchan = Srv.Store.User().GetByEmail(team.Id, loginId) + } else if method == model.USER_AUTH_SERVICE_USERNAME { + uchan = Srv.Store.User().GetByUsername(team.Id, loginId) + } else if method == model.USER_AUTH_SERVICE_LDAP { + uchan = Srv.Store.User().GetByAuth(team.Id, loginId, model.USER_AUTH_SERVICE_LDAP) + } + + rdata := map[string]string{} + if result := <-uchan; result.Err != nil { + rdata["mfa_required"] = "false" + } else { + rdata["mfa_required"] = strconv.FormatBool(result.Data.(*model.User).MfaActive) + } + w.Write([]byte(model.MapToJson(rdata))) +} diff --git a/api/user_test.go b/api/user_test.go index 86cda0390..33f3fdad4 100644 --- a/api/user_test.go +++ b/api/user_test.go @@ -1411,3 +1411,79 @@ func TestMeLoggedIn(t *testing.T) { } } } + +func TestGenerateMfaQrCode(t *testing.T) { + Setup() + + team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + rteam, _ := Client.CreateTeam(&team) + + user := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} + ruser, _ := Client.CreateUser(&user, "") + store.Must(Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id)) + + Client.Logout() + + if _, err := Client.GenerateMfaQrCode(); err == nil { + t.Fatal("should have failed - not logged in") + } + + Client.LoginByEmail(team.Name, user.Email, user.Password) + + if _, err := Client.GenerateMfaQrCode(); err == nil { + t.Fatal("should have failed - not licensed") + } + + // need to add more test cases when license and config can be configured for tests +} + +func TestUpdateMfa(t *testing.T) { + Setup() + + team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + rteam, _ := Client.CreateTeam(&team) + + user := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} + ruser, _ := Client.CreateUser(&user, "") + store.Must(Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id)) + + Client.Logout() + + if _, err := Client.UpdateMfa(true, "123456"); err == nil { + t.Fatal("should have failed - not logged in") + } + + Client.LoginByEmail(team.Name, user.Email, user.Password) + + if _, err := Client.UpdateMfa(true, ""); err == nil { + t.Fatal("should have failed - no token") + } + + if _, err := Client.UpdateMfa(true, "123456"); err == nil { + t.Fatal("should have failed - not licensed") + } + + // need to add more test cases when license and config can be configured for tests +} + +func TestCheckMfa(t *testing.T) { + Setup() + + team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + rteam, _ := Client.CreateTeam(&team) + + user := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} + ruser, _ := Client.CreateUser(&user, "") + store.Must(Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id)) + + if result, err := Client.CheckMfa(model.USER_AUTH_SERVICE_EMAIL, team.Name, user.Email); err != nil { + t.Fatal(err) + } else { + resp := result.Data.(map[string]string) + if resp["mfa_required"] != "false" { + t.Fatal("mfa should not be required") + } + } + + // need to add more test cases when license and config can be configured for tests +} diff --git a/config/config.json b/config/config.json index 65a61bb72..62dcfcffc 100644 --- a/config/config.json +++ b/config/config.json @@ -15,6 +15,7 @@ "EnableDeveloper": false, "EnableSecurityFixAlert": true, "EnableInsecureOutgoingConnections": false, + "EnableMultifactorAuthentication": false, "AllowCorsFrom": "", "SessionLengthWebInDays": 30, "SessionLengthMobileInDays": 30, diff --git a/einterfaces/mfa.go b/einterfaces/mfa.go new file mode 100644 index 000000000..0703fb766 --- /dev/null +++ b/einterfaces/mfa.go @@ -0,0 +1,25 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package einterfaces + +import ( + "github.com/mattermost/platform/model" +) + +type MfaInterface interface { + GenerateQrCode(team *model.Team, user *model.User) ([]byte, *model.AppError) + Activate(user *model.User, token string) *model.AppError + Deactivate(userId string) *model.AppError + ValidateToken(secret, token string) (bool, *model.AppError) +} + +var theMfaInterface MfaInterface + +func RegisterMfaInterface(newInterface MfaInterface) { + theMfaInterface = newInterface +} + +func GetMfaInterface() MfaInterface { + return theMfaInterface +} diff --git a/i18n/en.json b/i18n/en.json index 0207d660e..40bce082c 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1291,6 +1291,14 @@ "id": "api.templates.welcome_subject", "translation": "You joined {{ .TeamDisplayName }}" }, + { + "id": "api.user.update_mfa.not_available.app_error", + "translation": "MFA not configured or available on this server" + }, + { + "id": "api.user.generate_mfa_qr.not_available.app_error", + "translation": "MFA not configured or available on this server" + }, { "id": "api.user.add_direct_channels_and_forget.failed.error", "translation": "Failed to add direct channel preferences for user user_id=%s, team_id=%s, err=%v" @@ -1327,6 +1335,14 @@ "id": "api.user.authorize_oauth_user.unsupported.app_error", "translation": "Unsupported OAuth service provider" }, + { + "id": "api.user.check_user_mfa.not_available.app_error", + "translation": "MFA is not configured or supported on this server" + }, + { + "id": "api.user.check_user_mfa.bad_code.app_error", + "translation": "Invalid MFA token." + }, { "id": "api.user.check_user_login_attempts.too_many.app_error", "translation": "Your account is locked because of too many failed password attempts. Please reset your password." @@ -1739,6 +1755,42 @@ "id": "ent.compliance.run_started.info", "translation": "Compliance export started for job '{{.JobName}}' at '{{.FilePath}}'" }, + { + "id": "ent.mfa.license_disable.app_error", + "translation": "Your license does not support using multi-factor authentication" + }, + { + "id": "ent.mfa.generate_qr_code.create_code.app_error", + "translation": "Error generating QR code" + }, + { + "id": "ent.mfa.generate_qr_code.save_secret.app_error", + "translation": "Error saving the MFA secret" + }, + { + "id": "ent.mfa.activate.authenticate.app_error", + "translation": "Error attempting to authenticate MFA token" + }, + { + "id": "ent.mfa.activate.bad_token.app_error", + "translation": "Invalid MFA token" + }, + { + "id": "ent.mfa.activate.save_active.app_erro", + "translation": "Unable to update MFA active status for the user" + }, + { + "id": "ent.mfa.deactivate.save_active.app_erro", + "translation": "Unable to update MFA active status for the user" + }, + { + "id": "ent.mfa.deactivate.save_secret.app_error", + "translation": "Error clearing the MFA secret" + }, + { + "id": "ent.mfa.validate_token.authenticate.app_error", + "translation": "Error trying to authenticate MFA token" + }, { "id": "ent.ldap.do_login.bind_admin_user.app_error", "translation": "Unable to bind to LDAP server. Check BindUsername and BindPassword." @@ -3135,6 +3187,14 @@ "id": "store.sql_team.update_display_name.app_error", "translation": "We couldn't update the team name" }, + { + "id": "store.sql_user.update_mfa_secret.app_error", + "translation": "We encountered an error updating the user's MFA secret" + }, + { + "id": "store.sql_user.update_mfa_active.app_error", + "translation": "We encountered an error updating the user's MFA active status" + }, { "id": "store.sql_user.analytics_unique_user_count.app_error", "translation": "We couldn't get the unique user count" diff --git a/mattermost.go b/mattermost.go index c555862e9..d397a1ad8 100644 --- a/mattermost.go +++ b/mattermost.go @@ -29,7 +29,9 @@ import ( _ "github.com/mattermost/platform/model/gitlab" // Enterprise Deps + _ "github.com/dgryski/dgoogauth" _ "github.com/go-ldap/ldap" + _ "github.com/mattermost/rsc/qr" ) //ENTERPRISE_IMPORTS diff --git a/model/client.go b/model/client.go index ee26ae64e..960fe634b 100644 --- a/model/client.go +++ b/model/client.go @@ -301,6 +301,42 @@ func (c *Client) Logout() (*Result, *AppError) { } } +func (c *Client) CheckMfa(method, teamName, loginId string) (*Result, *AppError) { + m := make(map[string]string) + m["method"] = method + m["team_name"] = teamName + m["login_id"] = loginId + + if r, err := c.DoApiPost("/users/mfa", MapToJson(m)); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil + } +} + +func (c *Client) GenerateMfaQrCode() (*Result, *AppError) { + if r, err := c.DoApiGet("/users/generate_mfa_qr", "", ""); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), r.Body}, nil + } +} + +func (c *Client) UpdateMfa(activate bool, token string) (*Result, *AppError) { + m := make(map[string]interface{}) + m["activate"] = activate + m["token"] = token + + if r, err := c.DoApiPost("/users/update_mfa", StringInterfaceToJson(m)); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil + } +} + func (c *Client) SetOAuthToken(token string) { c.AuthToken = token c.AuthType = HEADER_TOKEN diff --git a/model/config.go b/model/config.go index 3ca241275..e7ab07f8c 100644 --- a/model/config.go +++ b/model/config.go @@ -46,6 +46,7 @@ type ServiceSettings struct { EnableDeveloper *bool EnableSecurityFixAlert *bool EnableInsecureOutgoingConnections *bool + EnableMultifactorAuthentication *bool AllowCorsFrom *string SessionLengthWebInDays *int SessionLengthMobileInDays *int @@ -275,6 +276,11 @@ func (o *Config) SetDefaults() { *o.ServiceSettings.EnableInsecureOutgoingConnections = false } + if o.ServiceSettings.EnableMultifactorAuthentication == nil { + o.ServiceSettings.EnableMultifactorAuthentication = new(bool) + *o.ServiceSettings.EnableMultifactorAuthentication = false + } + if o.TeamSettings.RestrictTeamNames == nil { o.TeamSettings.RestrictTeamNames = new(bool) *o.TeamSettings.RestrictTeamNames = true diff --git a/model/license.go b/model/license.go index 8461c9f76..cab22a685 100644 --- a/model/license.go +++ b/model/license.go @@ -34,6 +34,7 @@ type Customer struct { type Features struct { Users *int `json:"users"` LDAP *bool `json:"ldap"` + MFA *bool `json:"mfa"` GoogleSSO *bool `json:"google_sso"` Compliance *bool `json:"compliance"` } @@ -49,6 +50,11 @@ func (f *Features) SetDefaults() { *f.LDAP = true } + if f.MFA == nil { + f.MFA = new(bool) + *f.MFA = true + } + if f.GoogleSSO == nil { f.GoogleSSO = new(bool) *f.GoogleSSO = true diff --git a/model/user.go b/model/user.go index 675a1ded6..f8f2fdb70 100644 --- a/model/user.go +++ b/model/user.go @@ -15,17 +15,19 @@ import ( ) const ( - ROLE_TEAM_ADMIN = "admin" - ROLE_SYSTEM_ADMIN = "system_admin" - USER_AWAY_TIMEOUT = 5 * 60 * 1000 // 5 minutes - USER_OFFLINE_TIMEOUT = 1 * 60 * 1000 // 1 minute - USER_OFFLINE = "offline" - USER_AWAY = "away" - USER_ONLINE = "online" - USER_NOTIFY_ALL = "all" - USER_NOTIFY_MENTION = "mention" - USER_NOTIFY_NONE = "none" - DEFAULT_LOCALE = "en" + ROLE_TEAM_ADMIN = "admin" + ROLE_SYSTEM_ADMIN = "system_admin" + USER_AWAY_TIMEOUT = 5 * 60 * 1000 // 5 minutes + USER_OFFLINE_TIMEOUT = 1 * 60 * 1000 // 1 minute + USER_OFFLINE = "offline" + USER_AWAY = "away" + USER_ONLINE = "online" + USER_NOTIFY_ALL = "all" + USER_NOTIFY_MENTION = "mention" + USER_NOTIFY_NONE = "none" + DEFAULT_LOCALE = "en" + USER_AUTH_SERVICE_EMAIL = "email" + USER_AUTH_SERVICE_USERNAME = "username" ) type User struct { @@ -54,6 +56,8 @@ type User struct { LastPictureUpdate int64 `json:"last_picture_update,omitempty"` FailedAttempts int `json:"failed_attempts,omitempty"` Locale string `json:"locale"` + MfaActive bool `json:"mfa_active,omitempty"` + MfaSecret string `json:"mfa_secret,omitempty"` } // IsValid validates the user and returns an error if it isn't configured @@ -140,6 +144,8 @@ func (u *User) PreSave() { u.LastPasswordUpdate = u.CreateAt + u.MfaActive = false + if u.Locale == "" { u.Locale = DEFAULT_LOCALE } diff --git a/store/sql_user_store.go b/store/sql_user_store.go index 6062b8a6a..33d1887ad 100644 --- a/store/sql_user_store.go +++ b/store/sql_user_store.go @@ -40,6 +40,7 @@ func NewSqlUserStore(sqlStore *SqlStore) UserStore { table.ColMap("NotifyProps").SetMaxSize(2000) table.ColMap("ThemeProps").SetMaxSize(2000) table.ColMap("Locale").SetMaxSize(5) + table.ColMap("MfaSecret").SetMaxSize(128) table.SetUniqueTogether("Email", "TeamId") table.SetUniqueTogether("Username", "TeamId") } @@ -50,6 +51,9 @@ func NewSqlUserStore(sqlStore *SqlStore) UserStore { func (us SqlUserStore) UpgradeSchemaIfNeeded() { // ADDED for 2.0 REMOVE for 2.4 us.CreateColumnIfNotExists("Users", "Locale", "varchar(5)", "character varying(5)", model.DEFAULT_LOCALE) + // ADDED for 2.2 REMOVE for 2.6 + us.CreateColumnIfNotExists("Users", "MfaActive", "tinyint(1)", "boolean", "0") + us.CreateColumnIfNotExists("Users", "MfaSecret", "varchar(128)", "character varying(128)", "") } func (us SqlUserStore) CreateIndexesIfNotExists() { @@ -141,6 +145,8 @@ func (us SqlUserStore) Update(user *model.User, allowActiveUpdate bool) StoreCha user.LastPingAt = oldUser.LastPingAt user.EmailVerified = oldUser.EmailVerified user.FailedAttempts = oldUser.FailedAttempts + user.MfaSecret = oldUser.MfaSecret + user.MfaActive = oldUser.MfaActive if !allowActiveUpdate { user.Roles = oldUser.Roles @@ -346,6 +352,50 @@ func (us SqlUserStore) UpdateAuthData(userId, service, authData, email string) S return storeChannel } +func (us SqlUserStore) UpdateMfaSecret(userId, secret string) StoreChannel { + + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + updateAt := model.GetMillis() + + if _, err := us.GetMaster().Exec("UPDATE Users SET MfaSecret = :Secret, UpdateAt = :UpdateAt WHERE Id = :UserId", map[string]interface{}{"Secret": secret, "UpdateAt": updateAt, "UserId": userId}); err != nil { + result.Err = model.NewLocAppError("SqlUserStore.UpdateMfaSecret", "store.sql_user.update_mfa_secret.app_error", nil, "id="+userId+", "+err.Error()) + } else { + result.Data = userId + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (us SqlUserStore) UpdateMfaActive(userId string, active bool) StoreChannel { + + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + updateAt := model.GetMillis() + + if _, err := us.GetMaster().Exec("UPDATE Users SET MfaActive = :Active, UpdateAt = :UpdateAt WHERE Id = :UserId", map[string]interface{}{"Active": active, "UpdateAt": updateAt, "UserId": userId}); err != nil { + result.Err = model.NewLocAppError("SqlUserStore.UpdateMfaActive", "store.sql_user.update_mfa_active.app_error", nil, "id="+userId+", "+err.Error()) + } else { + result.Data = userId + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + func (us SqlUserStore) Get(id string) StoreChannel { storeChannel := make(StoreChannel) diff --git a/store/sql_user_store_test.go b/store/sql_user_store_test.go index 8f2366136..dcd2440ac 100644 --- a/store/sql_user_store_test.go +++ b/store/sql_user_store_test.go @@ -502,3 +502,47 @@ func TestUserUnreadCount(t *testing.T) { t.Fatal("should have 3 unread messages") } } + +func TestUserStoreUpdateMfaSecret(t *testing.T) { + Setup() + + u1 := model.User{} + u1.TeamId = model.NewId() + u1.Email = model.NewId() + Must(store.User().Save(&u1)) + + time.Sleep(100 * time.Millisecond) + + if err := (<-store.User().UpdateMfaSecret(u1.Id, "12345")).Err; err != nil { + t.Fatal(err) + } + + // should pass, no update will occur though + if err := (<-store.User().UpdateMfaSecret("junk", "12345")).Err; err != nil { + t.Fatal(err) + } +} + +func TestUserStoreUpdateMfaActive(t *testing.T) { + Setup() + + u1 := model.User{} + u1.TeamId = model.NewId() + u1.Email = model.NewId() + Must(store.User().Save(&u1)) + + time.Sleep(100 * time.Millisecond) + + if err := (<-store.User().UpdateMfaActive(u1.Id, true)).Err; err != nil { + t.Fatal(err) + } + + if err := (<-store.User().UpdateMfaActive(u1.Id, false)).Err; err != nil { + t.Fatal(err) + } + + // should pass, no update will occur though + if err := (<-store.User().UpdateMfaActive("junk", true)).Err; err != nil { + t.Fatal(err) + } +} diff --git a/store/store.go b/store/store.go index 7ec5ac3a5..323595ffb 100644 --- a/store/store.go +++ b/store/store.go @@ -117,6 +117,8 @@ type UserStore interface { UpdateUserAndSessionActivity(userId string, sessionId string, time int64) StoreChannel UpdatePassword(userId, newPassword string) StoreChannel UpdateAuthData(userId, service, authData, email string) StoreChannel + UpdateMfaSecret(userId, secret string) StoreChannel + UpdateMfaActive(userId string, active bool) StoreChannel Get(id string) StoreChannel GetProfiles(teamId string) StoreChannel GetByEmail(teamId string, email string) StoreChannel diff --git a/utils/config.go b/utils/config.go index 9624196be..93c8ffc7c 100644 --- a/utils/config.go +++ b/utils/config.go @@ -212,6 +212,7 @@ func getClientConfig(c *model.Config) map[string]string { props["EnableSignUpWithEmail"] = strconv.FormatBool(c.EmailSettings.EnableSignUpWithEmail) props["EnableSignInWithEmail"] = strconv.FormatBool(*c.EmailSettings.EnableSignInWithEmail) props["EnableSignInWithUsername"] = strconv.FormatBool(*c.EmailSettings.EnableSignInWithUsername) + props["EnableMultifactorAuthentication"] = strconv.FormatBool(*c.ServiceSettings.EnableMultifactorAuthentication) props["RequireEmailVerification"] = strconv.FormatBool(c.EmailSettings.RequireEmailVerification) props["FeedbackEmail"] = c.EmailSettings.FeedbackEmail diff --git a/utils/license.go b/utils/license.go index 1dc8bf025..217fd27ce 100644 --- a/utils/license.go +++ b/utils/license.go @@ -114,6 +114,7 @@ func getClientLicense(l *model.License) map[string]string { if IsLicensed { props["Users"] = strconv.Itoa(*l.Features.Users) props["LDAP"] = strconv.FormatBool(*l.Features.LDAP) + props["MFA"] = strconv.FormatBool(*l.Features.MFA) props["GoogleSSO"] = strconv.FormatBool(*l.Features.GoogleSSO) props["Compliance"] = strconv.FormatBool(*l.Features.Compliance) props["IssuedAt"] = strconv.FormatInt(l.IssuedAt, 10) diff --git a/webapp/components/admin_console/service_settings.jsx b/webapp/components/admin_console/service_settings.jsx index 881d22d76..41ea5ea34 100644 --- a/webapp/components/admin_console/service_settings.jsx +++ b/webapp/components/admin_console/service_settings.jsx @@ -84,6 +84,7 @@ class ServiceSettings extends React.Component { config.ServiceSettings.EnableDeveloper = ReactDOM.findDOMNode(this.refs.EnableDeveloper).checked; config.ServiceSettings.EnableSecurityFixAlert = ReactDOM.findDOMNode(this.refs.EnableSecurityFixAlert).checked; config.ServiceSettings.EnableInsecureOutgoingConnections = ReactDOM.findDOMNode(this.refs.EnableInsecureOutgoingConnections).checked; + config.ServiceSettings.EnableMultifactorAuthentication = ReactDOM.findDOMNode(this.refs.EnableMultifactorAuthentication).checked; config.ServiceSettings.EnableCommands = ReactDOM.findDOMNode(this.refs.EnableCommands).checked; config.ServiceSettings.EnableOnlyAdminIntegrations = ReactDOM.findDOMNode(this.refs.EnableOnlyAdminIntegrations).checked; @@ -173,6 +174,58 @@ class ServiceSettings extends React.Component { saveClass = 'btn btn-primary'; } + let mfaSetting; + if (global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.MFA === 'true') { + mfaSetting = ( +
+ +
+ + +

+ +

+
+
+ ); + } + return (
@@ -773,6 +826,8 @@ class ServiceSettings extends React.Component {
+ {mfaSetting} +