# HG changeset patch # User Paul Fisher # Date 1446398211 18000 # Node ID 00d30c67b56d7bbf160534c688ec4cf5f2897f55 # Parent 342f63116bfde3f1d344ecf82bed1206e8e585ce Put all the library stuff into multipass/file. diff -r 342f63116bfd -r 00d30c67b56d auth/auth.go --- a/auth/auth.go Fri Oct 30 00:18:13 2015 -0400 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,141 +0,0 @@ -// Package auth contains data structures for authenticating users. - -package auth - -import ( - "crypto/rand" - "encoding/base64" - "errors" - "math/big" - "strconv" - "strings" - - "golang.org/x/crypto/bcrypt" -) - -const ( - // default cost stolen from python bcrypt - bcryptCost = 12 - // we only generate passwords from lowercases for non-ambiguity - lowercases = "abcdefghijklmnopqrstuvwxyz" - template = "????-????-????-????" -) - -var ( - lowercaseLen *big.Int = big.NewInt(int64(len(lowercases))) - maxID = big.NewInt(0) -) - -var ( - WrongLengthError error = errors.New("multipass/auth: password entry must have 3 fields") - BadIDError = errors.New("multipass/auth: ID field invalid") - Base64Error = errors.New("multipass/auth: can't decode base64 data") - LongDescriptionError = errors.New("multipass/auth: description must be less than 255 bytes") -) - -func init() { - one := big.NewInt(1) - maxID.Lsh(one, 64) -} - -// Entry represents a single entry in the a multipass file. -type Entry struct { - id uint64 - hash string - description string -} - -// EntryFromShadow creates a new entry from a line in a multipass shadow file. -// The line should not end in a newline. -func EntryFromShadow(shadow string) (*Entry, error) { - segments := strings.Split(shadow, ":") - if len(segments) != 3 { - return nil, WrongLengthError - } - entry := new(Entry) - id, err := strconv.ParseUint(segments[0], 10, 64) - if err != nil { - return nil, BadIDError - } - entry.id = id - entry.hash = segments[1] - description, err := base64.StdEncoding.DecodeString(segments[2]) - if err != nil { - return nil, Base64Error - } - entry.description = string(description) - return entry, nil -} - -// NewEntry creates an Entry for the given description. -// It returns the Entry itself and a generated password. -func NewEntry(description string) (entry *Entry, password string, err error) { - if len(description) > 255 { - return nil, "", LongDescriptionError - } - passBytes := genPassword() - password = string(passBytes) - hashBytes, err := bcrypt.GenerateFromPassword(passBytes, bcryptCost) - if err != nil { - // This is very unexpected. - return nil, "", err - } - e := new(Entry) - e.id = newID() - e.hash = string(hashBytes) - e.description = description - return e, password, nil -} - -// ID is a unique 64-bit integer which identifies the entry. -func (e *Entry) ID() uint64 { - return e.id -} - -// Description is the user's description of their password. -func (e *Entry) Description() string { - return e.description -} - -// Authenticate tests whether the password is correct. -func (e *Entry) Authenticate(password string) bool { - err := bcrypt.CompareHashAndPassword([]byte(e.hash), []byte(password)) - return err == nil -} - -// Encode encodes this Entry to a bytestring for writing to a multipass shadow file. -func (e *Entry) Encode() string { - segments := []string{ - strconv.FormatUint(e.id, 10), - e.hash, - base64.StdEncoding.EncodeToString([]byte(e.description)), - } - return strings.Join(segments, ":") -} - -func genPassword() []byte { - password := []byte(template) - for i, chr := range password { - if chr == '?' { - password[i] = randChr() - } - } - return password -} - -func randChr() byte { - bigIdx, err := rand.Int(rand.Reader, lowercaseLen) - if err != nil { - panic("multipass/auth: can't get a random number") - } - idx := bigIdx.Int64() - return byte(lowercases[idx]) -} - -func newID() uint64 { - bigID, err := rand.Int(rand.Reader, maxID) - if err != nil { - panic("multipass/auth: can't get a random number") - } - return bigID.Uint64() -} diff -r 342f63116bfd -r 00d30c67b56d auth/auth_test.go --- a/auth/auth_test.go Fri Oct 30 00:18:13 2015 -0400 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,199 +0,0 @@ -package auth - -import ( - "regexp" - "testing" -) - -var passPattern *regexp.Regexp = regexp.MustCompile(`^(?:[a-z]{4}-){3}[a-z]{4}$`) - -const basicShadow = "9999:$2a$12$tcv2MrtXgibAJHsSwVfHiOevXBFmiGy0HTNoOB8QzIhEh46iWS1uC:YW55dGhpbmcgbW9yZSB0aGFuIDUgcmVwcyBpcyBjYXJkaW8=" -const anotherShadow = "1:$2a$12$lINQdYWHOcLKoqhNOr3mNOpZSAu5JOBS2F7T/VDfYn2rvv6qUJehG:" - -func TestEntryFromShadow(t *testing.T) { - cases := []struct { - shadow string - wantErr bool - username string - id uint64 - hash string - description string - rest []string - }{ - { - shadow: "1234:$2a$12$apFtWGXKtWBavVy5eo.22Ohs43GudT5IYTqyQkIBX9LpS7YtvKBpa:", - id: 1234, - hash: "$2a$12$apFtWGXKtWBavVy5eo.22Ohs43GudT5IYTqyQkIBX9LpS7YtvKBpa", - }, - { - shadow: basicShadow, - id: 9999, - hash: "$2a$12$tcv2MrtXgibAJHsSwVfHiOevXBFmiGy0HTNoOB8QzIhEh46iWS1uC", - description: "anything more than 5 reps is cardio", - }, - { - shadow: anotherShadow, - id: 1, - hash: "$2a$12$lINQdYWHOcLKoqhNOr3mNOpZSAu5JOBS2F7T/VDfYn2rvv6qUJehG", - }, - { - shadow: "one:bogushash:", - wantErr: true, - }, - { - shadow: "-1:bogushash:", - wantErr: true, - }, - { - shadow: "0:tooshort", - wantErr: true, - }, - { - shadow: "0:bogushash:invalid base64", - wantErr: true, - }, - { - shadow: "1:bogushash::more things", - wantErr: true, - }, - } - for _, c := range cases { - entry, err := EntryFromShadow(c.shadow) - if c.wantErr { - if err == nil { - t.Errorf("EntryFromShadow(%q) == _, nil; want non-nil err", c.shadow) - } - continue - } - if err != nil { - t.Errorf("EntryFromShadow(%q) == _, %q; want nil err", c.shadow, err) - } - if c.id != entry.id { - t.Errorf("EntryFromShadow(%q).id = %q; want %q", c.shadow, entry.id, c.id) - } - if c.hash != string(entry.hash) { - t.Errorf("EntryFromShadow(%q).password = %q; want %q", c.shadow, entry.hash, c.hash) - } - if c.description != entry.description { - t.Errorf("EntryFromShadow(%q).description = %q; want %q", c.shadow, entry.description, c.description) - } - } -} - -func TestNewEntry(t *testing.T) { - cases := []struct { - description string - wantErr bool - }{ - {"one", false}, - {"the other", false}, - {string(make([]byte, 1000)), true}, - } - for _, c := range cases { - entry, password, err := NewEntry(c.description) - if c.wantErr { - if err == nil { - t.Errorf("NewEntry(%q) = _, _, nil; want non-nil err", c.description) - } - continue - } - if err != nil { - t.Errorf("NewEntry(%q) = _, _, %q; want nil err", c.description, err) - } - if entry.id == 0 { - // This test has a 1/(2**64) chance of failing! :o - t.Errorf("NewEntry(_).id == 0, want nonzero") - } - if c.description != entry.description { - t.Errorf("NewEntry(%q).description = %q, want %q", - c.description, entry.description, c.description) - } - if !passPattern.MatchString(password) { - t.Errorf("NewEntry(_) = _, %q, _; wanted to match xxxx-xxxx-xxxx-xxxx", password) - } - if !entry.Authenticate(password) { - t.Errorf("NewEntry(%q).Authenticate(%q) failed", - c.description, password) - } - } -} - -func TestGenPassword(t *testing.T) { - p := genPassword() - if !passPattern.MatchString(string(p)) { - t.Errorf("genPassword() = %q; wanted to match xxxx-xxxx-xxxx-xxxx", p) - } -} - -func TestAuthenticate(t *testing.T) { - entry, password, err := NewEntry("") - if err != nil { - t.Errorf("Error building entry") - } - type testcase struct { - password string - want bool - } - - cases := []testcase{ - {password, true}, - {"not the password", false}, - } - for _, c := range cases { - got := entry.Authenticate(c.password) - if got != c.want { - t.Errorf("entry.Authenticate(%q) == %v, want %v", - c.password, got, c.want) - } - } - - entry, err = EntryFromShadow(basicShadow) - if err != nil { - t.Errorf("Error loading valid shadow") - } - - cases = []testcase{ - {"nocardio", true}, - {"not the password", false}, - } - for _, c := range cases { - got := entry.Authenticate(c.password) - if got != c.want { - t.Errorf("entry.Authenticate(%q) == %v, want %v", - c.password, got, c.want) - } - } -} - -func TestEncode(t *testing.T) { - // Crafted entry - shadowed, err := EntryFromShadow(basicShadow) - if err != nil { - t.Errorf("Error loading valid shadow") - } - anotherShadowed, err := EntryFromShadow(anotherShadow) - if err != nil { - t.Errorf("Error loading valid shadow") - } - cases := []struct { - entry *Entry - want string - }{ - { - &Entry{ - id: 6775, - hash: "bogushash", - description: "something", - }, - "6775:bogushash:c29tZXRoaW5n", - }, - {shadowed, basicShadow}, - {anotherShadowed, anotherShadow}, - } - for _, c := range cases { - got := string(c.entry.Encode()) - if got != c.want { - t.Errorf("entry.Encode() = %q, want %q", got, c.want) - } - } -} diff -r 342f63116bfd -r 00d30c67b56d cmds/multipass-add/add.go --- a/cmds/multipass-add/add.go Fri Oct 30 00:18:13 2015 -0400 +++ b/cmds/multipass-add/add.go Sun Nov 01 12:16:51 2015 -0500 @@ -7,7 +7,6 @@ "fmt" "os" - "pfish.zone/go/multipass/auth" "pfish.zone/go/multipass/file" ) @@ -25,7 +24,7 @@ os.Exit(1) } text = text[:len(text)-1] - entry, password, err := auth.NewEntry(text) + entry, password, err := file.NewEntry(text) if err != nil { fmt.Println(err.Error()) os.Exit(1) diff -r 342f63116bfd -r 00d30c67b56d file/auth.go --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/file/auth.go Sun Nov 01 12:16:51 2015 -0500 @@ -0,0 +1,139 @@ +package file + +import ( + "crypto/rand" + "encoding/base64" + "errors" + "math/big" + "strconv" + "strings" + + "golang.org/x/crypto/bcrypt" +) + +const ( + // default cost stolen from python bcrypt + bcryptCost = 12 + // we only generate passwords from lowercases for non-ambiguity + lowercases = "abcdefghijklmnopqrstuvwxyz" + template = "????-????-????-????" +) + +var ( + lowercaseLen *big.Int = big.NewInt(int64(len(lowercases))) + maxID = big.NewInt(0) +) + +var ( + WrongLengthError error = errors.New("multipass/auth: password entry must have 3 fields") + BadIDError = errors.New("multipass/auth: ID field invalid") + Base64Error = errors.New("multipass/auth: can't decode base64 data") + LongDescriptionError = errors.New("multipass/auth: description must be less than 255 bytes") +) + +func init() { + one := big.NewInt(1) + maxID.Lsh(one, 64) +} + +// Entry represents a single entry in the a multipass file. +type Entry struct { + id uint64 + hash string + description string +} + +// EntryFromShadow creates a new entry from a line in a multipass shadow file. +// The line should not end in a newline. +func EntryFromShadow(shadow string) (*Entry, error) { + segments := strings.Split(shadow, ":") + if len(segments) != 3 { + return nil, WrongLengthError + } + entry := new(Entry) + id, err := strconv.ParseUint(segments[0], 10, 64) + if err != nil { + return nil, BadIDError + } + entry.id = id + entry.hash = segments[1] + description, err := base64.StdEncoding.DecodeString(segments[2]) + if err != nil { + return nil, Base64Error + } + entry.description = string(description) + return entry, nil +} + +// NewEntry creates an Entry for the given description. +// It returns the Entry itself and a generated password. +func NewEntry(description string) (entry *Entry, password string, err error) { + if len(description) > 255 { + return nil, "", LongDescriptionError + } + passBytes := genPassword() + password = string(passBytes) + hashBytes, err := bcrypt.GenerateFromPassword(passBytes, bcryptCost) + if err != nil { + // This is very unexpected. + return nil, "", err + } + e := new(Entry) + e.id = newID() + e.hash = string(hashBytes) + e.description = description + return e, password, nil +} + +// ID is a unique 64-bit integer which identifies the entry. +func (e *Entry) ID() uint64 { + return e.id +} + +// Description is the user's description of their password. +func (e *Entry) Description() string { + return e.description +} + +// Authenticate tests whether the password is correct. +func (e *Entry) Authenticate(password string) bool { + err := bcrypt.CompareHashAndPassword([]byte(e.hash), []byte(password)) + return err == nil +} + +// Encode encodes this Entry to a bytestring for writing to a multipass shadow file. +func (e *Entry) Encode() string { + segments := []string{ + strconv.FormatUint(e.id, 10), + e.hash, + base64.StdEncoding.EncodeToString([]byte(e.description)), + } + return strings.Join(segments, ":") +} + +func genPassword() []byte { + password := []byte(template) + for i, chr := range password { + if chr == '?' { + password[i] = randChr() + } + } + return password +} + +func randChr() byte { + bigIdx, err := rand.Int(rand.Reader, lowercaseLen) + if err != nil { + panic("multipass/auth: can't get a random number") + } + idx := bigIdx.Int64() + return byte(lowercases[idx]) +} + +func newID() uint64 { + bigID, err := rand.Int(rand.Reader, maxID) + if err != nil { + panic("multipass/auth: can't get a random number") + } + return bigID.Uint64() +} diff -r 342f63116bfd -r 00d30c67b56d file/auth_test.go --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/file/auth_test.go Sun Nov 01 12:16:51 2015 -0500 @@ -0,0 +1,199 @@ +package file + +import ( + "regexp" + "testing" +) + +var passPattern *regexp.Regexp = regexp.MustCompile(`^(?:[a-z]{4}-){3}[a-z]{4}$`) + +const basicShadow = "9999:$2a$12$tcv2MrtXgibAJHsSwVfHiOevXBFmiGy0HTNoOB8QzIhEh46iWS1uC:YW55dGhpbmcgbW9yZSB0aGFuIDUgcmVwcyBpcyBjYXJkaW8=" +const anotherShadow = "1:$2a$12$lINQdYWHOcLKoqhNOr3mNOpZSAu5JOBS2F7T/VDfYn2rvv6qUJehG:" + +func TestEntryFromShadow(t *testing.T) { + cases := []struct { + shadow string + wantErr bool + username string + id uint64 + hash string + description string + rest []string + }{ + { + shadow: "1234:$2a$12$apFtWGXKtWBavVy5eo.22Ohs43GudT5IYTqyQkIBX9LpS7YtvKBpa:", + id: 1234, + hash: "$2a$12$apFtWGXKtWBavVy5eo.22Ohs43GudT5IYTqyQkIBX9LpS7YtvKBpa", + }, + { + shadow: basicShadow, + id: 9999, + hash: "$2a$12$tcv2MrtXgibAJHsSwVfHiOevXBFmiGy0HTNoOB8QzIhEh46iWS1uC", + description: "anything more than 5 reps is cardio", + }, + { + shadow: anotherShadow, + id: 1, + hash: "$2a$12$lINQdYWHOcLKoqhNOr3mNOpZSAu5JOBS2F7T/VDfYn2rvv6qUJehG", + }, + { + shadow: "one:bogushash:", + wantErr: true, + }, + { + shadow: "-1:bogushash:", + wantErr: true, + }, + { + shadow: "0:tooshort", + wantErr: true, + }, + { + shadow: "0:bogushash:invalid base64", + wantErr: true, + }, + { + shadow: "1:bogushash::more things", + wantErr: true, + }, + } + for _, c := range cases { + entry, err := EntryFromShadow(c.shadow) + if c.wantErr { + if err == nil { + t.Errorf("EntryFromShadow(%q) == _, nil; want non-nil err", c.shadow) + } + continue + } + if err != nil { + t.Errorf("EntryFromShadow(%q) == _, %q; want nil err", c.shadow, err) + } + if c.id != entry.id { + t.Errorf("EntryFromShadow(%q).id = %q; want %q", c.shadow, entry.id, c.id) + } + if c.hash != string(entry.hash) { + t.Errorf("EntryFromShadow(%q).password = %q; want %q", c.shadow, entry.hash, c.hash) + } + if c.description != entry.description { + t.Errorf("EntryFromShadow(%q).description = %q; want %q", c.shadow, entry.description, c.description) + } + } +} + +func TestNewEntry(t *testing.T) { + cases := []struct { + description string + wantErr bool + }{ + {"one", false}, + {"the other", false}, + {string(make([]byte, 1000)), true}, + } + for _, c := range cases { + entry, password, err := NewEntry(c.description) + if c.wantErr { + if err == nil { + t.Errorf("NewEntry(%q) = _, _, nil; want non-nil err", c.description) + } + continue + } + if err != nil { + t.Errorf("NewEntry(%q) = _, _, %q; want nil err", c.description, err) + } + if entry.id == 0 { + // This test has a 1/(2**64) chance of failing! :o + t.Errorf("NewEntry(_).id == 0, want nonzero") + } + if c.description != entry.description { + t.Errorf("NewEntry(%q).description = %q, want %q", + c.description, entry.description, c.description) + } + if !passPattern.MatchString(password) { + t.Errorf("NewEntry(_) = _, %q, _; wanted to match xxxx-xxxx-xxxx-xxxx", password) + } + if !entry.Authenticate(password) { + t.Errorf("NewEntry(%q).Authenticate(%q) failed", + c.description, password) + } + } +} + +func TestGenPassword(t *testing.T) { + p := genPassword() + if !passPattern.MatchString(string(p)) { + t.Errorf("genPassword() = %q; wanted to match xxxx-xxxx-xxxx-xxxx", p) + } +} + +func TestAuthenticate(t *testing.T) { + entry, password, err := NewEntry("") + if err != nil { + t.Errorf("Error building entry") + } + type testcase struct { + password string + want bool + } + + cases := []testcase{ + {password, true}, + {"not the password", false}, + } + for _, c := range cases { + got := entry.Authenticate(c.password) + if got != c.want { + t.Errorf("entry.Authenticate(%q) == %v, want %v", + c.password, got, c.want) + } + } + + entry, err = EntryFromShadow(basicShadow) + if err != nil { + t.Errorf("Error loading valid shadow") + } + + cases = []testcase{ + {"nocardio", true}, + {"not the password", false}, + } + for _, c := range cases { + got := entry.Authenticate(c.password) + if got != c.want { + t.Errorf("entry.Authenticate(%q) == %v, want %v", + c.password, got, c.want) + } + } +} + +func TestEncode(t *testing.T) { + // Crafted entry + shadowed, err := EntryFromShadow(basicShadow) + if err != nil { + t.Errorf("Error loading valid shadow") + } + anotherShadowed, err := EntryFromShadow(anotherShadow) + if err != nil { + t.Errorf("Error loading valid shadow") + } + cases := []struct { + entry *Entry + want string + }{ + { + &Entry{ + id: 6775, + hash: "bogushash", + description: "something", + }, + "6775:bogushash:c29tZXRoaW5n", + }, + {shadowed, basicShadow}, + {anotherShadowed, anotherShadow}, + } + for _, c := range cases { + got := string(c.entry.Encode()) + if got != c.want { + t.Errorf("entry.Encode() = %q, want %q", got, c.want) + } + } +} diff -r 342f63116bfd -r 00d30c67b56d file/file.go --- a/file/file.go Fri Oct 30 00:18:13 2015 -0400 +++ b/file/file.go Sun Nov 01 12:16:51 2015 -0500 @@ -2,7 +2,7 @@ // // A password file contains multiple passwords for a single user. // It starts with a banner that indicates the version of the file, -// then has entries in the format specified by auth.Entry. +// then has entries in the format specified by Entry. package file @@ -14,7 +14,6 @@ "time" "golang.org/x/sys/unix" - "pfish.zone/go/multipass/auth" ) const ( @@ -73,7 +72,7 @@ defer file.Close() for scanner.Scan() { - entry, err := auth.EntryFromShadow(scanner.Text()) + entry, err := EntryFromShadow(scanner.Text()) // Skip invalid lines. if err != nil { continue @@ -85,7 +84,7 @@ return false, nil } -func (f *ShadowFile) Add(entry *auth.Entry) error { +func (f *ShadowFile) Add(entry *Entry) error { handle, err := f.openWrite() if err != nil { return err @@ -107,17 +106,17 @@ return handle.finalize() } -func (f *ShadowFile) AllEntries() ([]*auth.Entry, error) { +func (f *ShadowFile) AllEntries() ([]*Entry, error) { file, scanner, err := f.open() if err != nil { return nil, err } defer file.Close() - var entries []*auth.Entry + var entries []*Entry for scanner.Scan() { - entry, err := auth.EntryFromShadow(scanner.Text()) + entry, err := EntryFromShadow(scanner.Text()) // Skip invalid lines. if err != nil { continue @@ -242,11 +241,11 @@ return h.scanner != nil && h.scanner.Scan() } -func (h *writeHandle) entry() (*auth.Entry, error) { - return auth.EntryFromShadow(h.scanner.Text()) +func (h *writeHandle) entry() (*Entry, error) { + return EntryFromShadow(h.scanner.Text()) } -func (h *writeHandle) write(entry *auth.Entry) error { +func (h *writeHandle) write(entry *Entry) error { if _, err := h.writer.WriteString(entry.Encode()); err != nil { return err } diff -r 342f63116bfd -r 00d30c67b56d file/file_test.go --- a/file/file_test.go Fri Oct 30 00:18:13 2015 -0400 +++ b/file/file_test.go Sun Nov 01 12:16:51 2015 -0500 @@ -7,8 +7,6 @@ "path" "testing" "time" - - "pfish.zone/go/multipass/auth" ) var tempdir string @@ -21,17 +19,17 @@ ) var ( - aSpooky *auth.Entry - aWhatever *auth.Entry + aSpooky *Entry + aWhatever *Entry ) func init() { - s, err := auth.EntryFromShadow(shadowSpooky) + s, err := EntryFromShadow(shadowSpooky) if err != nil { panic(err.Error()) } aSpooky = s - w, err := auth.EntryFromShadow(shadowWhatever) + w, err := EntryFromShadow(shadowWhatever) if err != nil { panic(err.Error()) } @@ -188,15 +186,15 @@ } } -func TestAuthenticate(t *testing.T) { +func TestAuthenticateFile(t *testing.T) { f := mktest(t, "auth") defer f.cleanup() s := New(f.filepath()) - eA, passA, err := auth.NewEntry("a") + eA, passA, err := NewEntry("a") if err != nil { t.Fatalf(err.Error()) } - eB, passB, err := auth.NewEntry("b") + eB, passB, err := NewEntry("b") if err != nil { t.Fatalf(err.Error()) }