Merge pull request #321 from MichaelMure/cred-password

auth: refactor and introduce Login and LoginPassword, salt IDs
This commit is contained in:
Michael Muré 2020-02-14 17:04:06 +01:00 committed by GitHub
commit 2df72942f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 377 additions and 78 deletions

View File

@ -1,6 +1,8 @@
package auth
import (
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"regexp"
@ -16,6 +18,7 @@ const (
configKeyKind = "kind"
configKeyTarget = "target"
configKeyCreateTime = "createtime"
configKeySalt = "salt"
configKeyPrefixMeta = "meta."
MetaKeyLogin = "login"
@ -26,6 +29,7 @@ type CredentialKind string
const (
KindToken CredentialKind = "token"
KindLogin CredentialKind = "login"
KindLoginPassword CredentialKind = "login-password"
)
@ -37,9 +41,10 @@ func NewErrMultipleMatchCredential(matching []entity.Id) *entity.ErrMultipleMatc
type Credential interface {
ID() entity.Id
Target() string
Kind() CredentialKind
Target() string
CreateTime() time.Time
Salt() []byte
Validate() error
Metadata() map[string]string
@ -47,7 +52,7 @@ type Credential interface {
SetMetadata(key string, value string)
// Return all the specific properties of the credential that need to be saved into the configuration.
// This does not include Target, Kind, CreateTime and Metadata.
// This does not include Target, Kind, CreateTime, Metadata or Salt.
toConfig() map[string]string
}
@ -108,15 +113,23 @@ func loadFromConfig(rawConfigs map[string]string, id entity.Id) (Credential, err
}
var cred Credential
var err error
switch CredentialKind(configs[configKeyKind]) {
case KindToken:
cred = NewTokenFromConfig(configs)
cred, err = NewTokenFromConfig(configs)
case KindLogin:
cred, err = NewLoginFromConfig(configs)
case KindLoginPassword:
cred, err = NewLoginPasswordFromConfig(configs)
default:
return nil, fmt.Errorf("unknown credential type %s", configs[configKeyKind])
}
if err != nil {
return nil, fmt.Errorf("loading credential: %v", err)
}
return cred, nil
}
@ -134,6 +147,23 @@ func metaFromConfig(configs map[string]string) map[string]string {
return result
}
func makeSalt() []byte {
result := make([]byte, 16)
_, err := rand.Read(result)
if err != nil {
panic(err)
}
return result
}
func saltFromConfig(configs map[string]string) ([]byte, error) {
val, ok := configs[configKeySalt]
if !ok {
return nil, fmt.Errorf("no credential salt found")
}
return base64.StdEncoding.DecodeString(val)
}
// List load all existing credentials
func List(repo repository.RepoConfig, opts ...Option) ([]Credential, error) {
rawConfigs, err := repo.GlobalConfig().ReadAll(configKeyPrefix + ".")
@ -211,6 +241,16 @@ func Store(repo repository.RepoConfig, cred Credential) error {
return err
}
// Salt
if len(cred.Salt()) != 16 {
panic("credentials need to be salted")
}
encoded := base64.StdEncoding.EncodeToString(cred.Salt())
err = repo.GlobalConfig().StoreString(prefix+configKeySalt, encoded)
if err != nil {
return err
}
// Metadata
for key, val := range cred.Metadata() {
err := repo.GlobalConfig().StoreString(prefix+configKeyPrefixMeta+key, val)

View File

@ -0,0 +1,90 @@
package auth
import (
"fmt"
"time"
"github.com/MichaelMure/git-bug/bridge/core"
"github.com/MichaelMure/git-bug/repository"
)
type credentialBase struct {
target string
createTime time.Time
salt []byte
meta map[string]string
}
func newCredentialBase(target string) *credentialBase {
return &credentialBase{
target: target,
createTime: time.Now(),
salt: makeSalt(),
}
}
func newCredentialBaseFromConfig(conf map[string]string) (*credentialBase, error) {
base := &credentialBase{
target: conf[configKeyTarget],
meta: metaFromConfig(conf),
}
if createTime, ok := conf[configKeyCreateTime]; ok {
t, err := repository.ParseTimestamp(createTime)
if err != nil {
return nil, err
}
base.createTime = t
} else {
return nil, fmt.Errorf("missing create time")
}
salt, err := saltFromConfig(conf)
if err != nil {
return nil, err
}
base.salt = salt
return base, nil
}
func (cb *credentialBase) Target() string {
return cb.target
}
func (cb *credentialBase) CreateTime() time.Time {
return cb.createTime
}
func (cb *credentialBase) Salt() []byte {
return cb.salt
}
func (cb *credentialBase) validate() error {
if cb.target == "" {
return fmt.Errorf("missing target")
}
if cb.createTime.IsZero() || cb.createTime.Equal(time.Time{}) {
return fmt.Errorf("missing creation time")
}
if !core.TargetExist(cb.target) {
return fmt.Errorf("unknown target")
}
return nil
}
func (cb *credentialBase) Metadata() map[string]string {
return cb.meta
}
func (cb *credentialBase) GetMetadata(key string) (string, bool) {
val, ok := cb.meta[key]
return val, ok
}
func (cb *credentialBase) SetMetadata(key string, value string) {
if cb.meta == nil {
cb.meta = make(map[string]string)
}
cb.meta[key] = value
}

View File

@ -14,7 +14,7 @@ func TestCredential(t *testing.T) {
repo := repository.NewMockRepoForTest()
storeToken := func(val string, target string) *Token {
token := NewToken(val, target)
token := NewToken(target, val)
err := Store(repo, token)
require.NoError(t, err)
return token
@ -100,3 +100,25 @@ func sameIds(t *testing.T, a []Credential, b []Credential) {
assert.ElementsMatch(t, ids(a), ids(b))
}
func testCredentialSerial(t *testing.T, original Credential) Credential {
repo := repository.NewMockRepoForTest()
original.SetMetadata("test", "value")
assert.NotEmpty(t, original.ID().String())
assert.NotEmpty(t, original.Salt())
assert.NoError(t, Store(repo, original))
loaded, err := LoadWithId(repo, original.ID())
assert.NoError(t, err)
assert.Equal(t, original.ID(), loaded.ID())
assert.Equal(t, original.Kind(), loaded.Kind())
assert.Equal(t, original.Target(), loaded.Target())
assert.Equal(t, original.CreateTime().Unix(), loaded.CreateTime().Unix())
assert.Equal(t, original.Salt(), loaded.Salt())
assert.Equal(t, original.Metadata(), loaded.Metadata())
return loaded
}

67
bridge/core/auth/login.go Normal file
View File

@ -0,0 +1,67 @@
package auth
import (
"crypto/sha256"
"fmt"
"github.com/MichaelMure/git-bug/entity"
)
const (
configKeyLoginLogin = "login"
)
var _ Credential = &Login{}
type Login struct {
*credentialBase
Login string
}
func NewLogin(target, login string) *Login {
return &Login{
credentialBase: newCredentialBase(target),
Login: login,
}
}
func NewLoginFromConfig(conf map[string]string) (*Login, error) {
base, err := newCredentialBaseFromConfig(conf)
if err != nil {
return nil, err
}
return &Login{
credentialBase: base,
Login: conf[configKeyLoginLogin],
}, nil
}
func (lp *Login) ID() entity.Id {
h := sha256.New()
_, _ = h.Write(lp.salt)
_, _ = h.Write([]byte(lp.target))
_, _ = h.Write([]byte(lp.Login))
return entity.Id(fmt.Sprintf("%x", h.Sum(nil)))
}
func (lp *Login) Kind() CredentialKind {
return KindLogin
}
func (lp *Login) Validate() error {
err := lp.credentialBase.validate()
if err != nil {
return err
}
if lp.Login == "" {
return fmt.Errorf("missing login")
}
return nil
}
func (lp *Login) toConfig() map[string]string {
return map[string]string{
configKeyLoginLogin: lp.Login,
}
}

View File

@ -0,0 +1,76 @@
package auth
import (
"crypto/sha256"
"fmt"
"github.com/MichaelMure/git-bug/entity"
)
const (
configKeyLoginPasswordLogin = "login"
configKeyLoginPasswordPassword = "password"
)
var _ Credential = &LoginPassword{}
type LoginPassword struct {
*credentialBase
Login string
Password string
}
func NewLoginPassword(target, login, password string) *LoginPassword {
return &LoginPassword{
credentialBase: newCredentialBase(target),
Login: login,
Password: password,
}
}
func NewLoginPasswordFromConfig(conf map[string]string) (*LoginPassword, error) {
base, err := newCredentialBaseFromConfig(conf)
if err != nil {
return nil, err
}
return &LoginPassword{
credentialBase: base,
Login: conf[configKeyLoginPasswordLogin],
Password: conf[configKeyLoginPasswordPassword],
}, nil
}
func (lp *LoginPassword) ID() entity.Id {
h := sha256.New()
_, _ = h.Write(lp.salt)
_, _ = h.Write([]byte(lp.target))
_, _ = h.Write([]byte(lp.Login))
_, _ = h.Write([]byte(lp.Password))
return entity.Id(fmt.Sprintf("%x", h.Sum(nil)))
}
func (lp *LoginPassword) Kind() CredentialKind {
return KindLoginPassword
}
func (lp *LoginPassword) Validate() error {
err := lp.credentialBase.validate()
if err != nil {
return err
}
if lp.Login == "" {
return fmt.Errorf("missing login")
}
if lp.Password == "" {
return fmt.Errorf("missing password")
}
return nil
}
func (lp *LoginPassword) toConfig() map[string]string {
return map[string]string{
configKeyLoginPasswordLogin: lp.Login,
configKeyLoginPasswordPassword: lp.Password,
}
}

View File

@ -0,0 +1,14 @@
package auth
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestLoginPasswordSerial(t *testing.T) {
original := NewLoginPassword("github", "jean", "jacques")
loaded := testCredentialSerial(t, original)
assert.Equal(t, original.Login, loaded.(*LoginPassword).Login)
assert.Equal(t, original.Password, loaded.(*LoginPassword).Password)
}

View File

@ -0,0 +1,13 @@
package auth
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestLoginSerial(t *testing.T) {
original := NewLogin("github", "jean")
loaded := testCredentialSerial(t, original)
assert.Equal(t, original.Login, loaded.(*Login).Login)
}

View File

@ -3,104 +3,68 @@ package auth
import (
"crypto/sha256"
"fmt"
"time"
"github.com/MichaelMure/git-bug/bridge/core"
"github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/repository"
)
const (
tokenValueKey = "value"
configKeyTokenValue = "value"
)
var _ Credential = &Token{}
// Token holds an API access token data
type Token struct {
target string
createTime time.Time
Value string
meta map[string]string
*credentialBase
Value string
}
// NewToken instantiate a new token
func NewToken(value, target string) *Token {
func NewToken(target, value string) *Token {
return &Token{
target: target,
createTime: time.Now(),
Value: value,
credentialBase: newCredentialBase(target),
Value: value,
}
}
func NewTokenFromConfig(conf map[string]string) *Token {
token := &Token{}
token.target = conf[configKeyTarget]
if createTime, ok := conf[configKeyCreateTime]; ok {
if t, err := repository.ParseTimestamp(createTime); err == nil {
token.createTime = t
}
func NewTokenFromConfig(conf map[string]string) (*Token, error) {
base, err := newCredentialBaseFromConfig(conf)
if err != nil {
return nil, err
}
token.Value = conf[tokenValueKey]
token.meta = metaFromConfig(conf)
return token
return &Token{
credentialBase: base,
Value: conf[configKeyTokenValue],
}, nil
}
func (t *Token) ID() entity.Id {
sum := sha256.Sum256([]byte(t.target + t.Value))
return entity.Id(fmt.Sprintf("%x", sum))
}
func (t *Token) Target() string {
return t.target
h := sha256.New()
_, _ = h.Write(t.salt)
_, _ = h.Write([]byte(t.target))
_, _ = h.Write([]byte(t.Value))
return entity.Id(fmt.Sprintf("%x", h.Sum(nil)))
}
func (t *Token) Kind() CredentialKind {
return KindToken
}
func (t *Token) CreateTime() time.Time {
return t.createTime
}
// Validate ensure token important fields are valid
func (t *Token) Validate() error {
err := t.credentialBase.validate()
if err != nil {
return err
}
if t.Value == "" {
return fmt.Errorf("missing value")
}
if t.target == "" {
return fmt.Errorf("missing target")
}
if t.createTime.IsZero() || t.createTime.Equal(time.Time{}) {
return fmt.Errorf("missing creation time")
}
if !core.TargetExist(t.target) {
return fmt.Errorf("unknown target")
}
return nil
}
func (t *Token) Metadata() map[string]string {
return t.meta
}
func (t *Token) GetMetadata(key string) (string, bool) {
val, ok := t.meta[key]
return val, ok
}
func (t *Token) SetMetadata(key string, value string) {
if t.meta == nil {
t.meta = make(map[string]string)
}
t.meta[key] = value
}
func (t *Token) toConfig() map[string]string {
return map[string]string{
tokenValueKey: t.Value,
configKeyTokenValue: t.Value,
}
}

View File

@ -0,0 +1,13 @@
package auth
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestTokenSerial(t *testing.T) {
original := NewToken("github", "value")
loaded := testCredentialSerial(t, original)
assert.Equal(t, original.Value, loaded.(*Token).Value)
}

View File

@ -86,7 +86,7 @@ func (g *Github) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor
}
login = l
case params.TokenRaw != "":
token := auth.NewToken(params.TokenRaw, target)
token := auth.NewToken(target, params.TokenRaw)
login, err = getLoginFromToken(token)
if err != nil {
return nil, err
@ -296,7 +296,7 @@ func promptTokenOptions(repo repository.RepoConfig, login, owner, project string
if err != nil {
return nil, err
}
token := auth.NewToken(value, target)
token := auth.NewToken(target, value)
token.SetMetadata(auth.MetaKeyLogin, login)
return token, nil
default:
@ -327,7 +327,7 @@ func promptToken() (*auth.Token, error) {
if !re.MatchString(value) {
return "token has incorrect format", nil
}
login, err = getLoginFromToken(auth.NewToken(value, target))
login, err = getLoginFromToken(auth.NewToken(target, value))
if err != nil {
return fmt.Sprintf("token is invalid: %v", err), nil
}
@ -339,7 +339,7 @@ func promptToken() (*auth.Token, error) {
return nil, err
}
token := auth.NewToken(rawToken, target)
token := auth.NewToken(target, rawToken)
token.SetMetadata(auth.MetaKeyLogin, login)
return token, nil

View File

@ -154,8 +154,8 @@ func TestValidateProject(t *testing.T) {
t.Skip("Env var GITHUB_TOKEN_PUBLIC missing")
}
tokenPrivate := auth.NewToken(envPrivate, target)
tokenPublic := auth.NewToken(envPublic, target)
tokenPrivate := auth.NewToken(target, envPrivate)
tokenPublic := auth.NewToken(target, envPublic)
type args struct {
owner string

View File

@ -157,7 +157,7 @@ func TestPushPull(t *testing.T) {
defer backend.Close()
interrupt.RegisterCleaner(backend.Close)
token := auth.NewToken(envToken, target)
token := auth.NewToken(target, envToken)
token.SetMetadata(auth.MetaKeyLogin, login)
err = auth.Store(repo, token)
require.NoError(t, err)

View File

@ -144,7 +144,7 @@ func Test_Importer(t *testing.T) {
login := "test-identity"
author.SetMetadata(metaKeyGithubLogin, login)
token := auth.NewToken(envToken, target)
token := auth.NewToken(target, envToken)
token.SetMetadata(auth.MetaKeyLogin, login)
err = auth.Store(repo, token)
require.NoError(t, err)

View File

@ -83,7 +83,7 @@ func (g *Gitlab) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor
}
login = l
case params.TokenRaw != "":
token := auth.NewToken(params.TokenRaw, target)
token := auth.NewToken(target, params.TokenRaw)
login, err = getLoginFromToken(baseUrl, token)
if err != nil {
return nil, err
@ -265,7 +265,7 @@ func promptToken(baseUrl string) (*auth.Token, error) {
if !re.MatchString(value) {
return "token has incorrect format", nil
}
login, err = getLoginFromToken(baseUrl, auth.NewToken(value, target))
login, err = getLoginFromToken(baseUrl, auth.NewToken(target, value))
if err != nil {
return fmt.Sprintf("token is invalid: %v", err), nil
}
@ -277,7 +277,7 @@ func promptToken(baseUrl string) (*auth.Token, error) {
return nil, err
}
token := auth.NewToken(rawToken, target)
token := auth.NewToken(target, rawToken)
token.SetMetadata(auth.MetaKeyLogin, login)
token.SetMetadata(auth.MetaKeyBaseURL, baseUrl)

View File

@ -162,7 +162,7 @@ func TestPushPull(t *testing.T) {
defer backend.Close()
interrupt.RegisterCleaner(backend.Close)
token := auth.NewToken(envToken, target)
token := auth.NewToken(target, envToken)
token.SetMetadata(auth.MetaKeyLogin, login)
token.SetMetadata(auth.MetaKeyBaseURL, defaultBaseURL)
err = auth.Store(repo, token)

View File

@ -98,7 +98,7 @@ func TestImport(t *testing.T) {
login := "test-identity"
author.SetMetadata(metaKeyGitlabLogin, login)
token := auth.NewToken(envToken, target)
token := auth.NewToken(target, envToken)
token.SetMetadata(auth.MetaKeyLogin, login)
token.SetMetadata(auth.MetaKeyBaseURL, defaultBaseURL)
err = auth.Store(repo, token)

View File

@ -86,7 +86,7 @@ func runBridgeTokenAdd(cmd *cobra.Command, args []string) error {
}
}
token := auth.NewToken(value, bridgeAuthAddTokenTarget)
token := auth.NewToken(bridgeAuthAddTokenTarget, value)
token.SetMetadata(auth.MetaKeyLogin, bridgeAuthAddTokenLogin)
if err := token.Validate(); err != nil {