| @@ -3,7 +3,7 @@ module code.gitea.io/gitea | |||
| go 1.12 | |||
| require ( | |||
| code.gitea.io/sdk v0.0.0-20190416172854-7d954d775498 | |||
| code.gitea.io/sdk v0.0.0-20190419065346-2858b80da5f7 | |||
| github.com/BurntSushi/toml v0.3.1 // indirect | |||
| github.com/PuerkitoBio/goquery v0.0.0-20170324135448-ed7d758e9a34 | |||
| github.com/RoaringBitmap/roaring v0.4.7 // indirect | |||
| @@ -1,6 +1,6 @@ | |||
| cloud.google.com/go v0.30.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= | |||
| code.gitea.io/sdk v0.0.0-20190416172854-7d954d775498 h1:rcjwXMYIjYts88akPiyy/GB+imecpf159jojChciEEw= | |||
| code.gitea.io/sdk v0.0.0-20190416172854-7d954d775498/go.mod h1:5bZt0dRznpn2JysytQnV0yCru3FwDv9O5G91jo+lDAk= | |||
| code.gitea.io/sdk v0.0.0-20190419065346-2858b80da5f7 h1:YggbbCVgggcOjKYmcB2wVOsEtJHgHUNFFJZDB6QcYTg= | |||
| code.gitea.io/sdk v0.0.0-20190419065346-2858b80da5f7/go.mod h1:5bZt0dRznpn2JysytQnV0yCru3FwDv9O5G91jo+lDAk= | |||
| github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= | |||
| github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= | |||
| github.com/PuerkitoBio/goquery v0.0.0-20170324135448-ed7d758e9a34 h1:UsHpWO0Elp6NaWVARdZHjiYwkhrspHVEGsyIKPb9OI8= | |||
| @@ -26,10 +26,10 @@ func TestAPICreateAndDeleteToken(t *testing.T) { | |||
| var newAccessToken api.AccessToken | |||
| DecodeJSON(t, resp, &newAccessToken) | |||
| models.AssertExistsAndLoadBean(t, &models.AccessToken{ | |||
| ID: newAccessToken.ID, | |||
| Name: newAccessToken.Name, | |||
| Sha1: newAccessToken.Sha1, | |||
| UID: user.ID, | |||
| ID: newAccessToken.ID, | |||
| Name: newAccessToken.Name, | |||
| Token: newAccessToken.Token, | |||
| UID: user.ID, | |||
| }) | |||
| req = NewRequestf(t, "DELETE", "/api/v1/users/user1/tokens/%d", newAccessToken.ID) | |||
| @@ -507,7 +507,7 @@ func (err ErrDeployKeyNameAlreadyUsed) Error() string { | |||
| // ErrAccessTokenNotExist represents a "AccessTokenNotExist" kind of error. | |||
| type ErrAccessTokenNotExist struct { | |||
| SHA string | |||
| Token string | |||
| } | |||
| // IsErrAccessTokenNotExist checks if an error is a ErrAccessTokenNotExist. | |||
| @@ -517,7 +517,7 @@ func IsErrAccessTokenNotExist(err error) bool { | |||
| } | |||
| func (err ErrAccessTokenNotExist) Error() string { | |||
| return fmt.Sprintf("access token does not exist [sha: %s]", err.SHA) | |||
| return fmt.Sprintf("access token does not exist [sha: %s]", err.Token) | |||
| } | |||
| // ErrAccessTokenEmpty represents a "AccessTokenEmpty" kind of error. | |||
| @@ -2,7 +2,10 @@ | |||
| id: 1 | |||
| uid: 1 | |||
| name: Token A | |||
| sha1: hash1 | |||
| #token: d2c6c1ba3890b309189a8e618c72a162e4efbf36 | |||
| token_hash: 2b3668e11cb82d3af8c6e4524fc7841297668f5008d1626f0ad3417e9fa39af84c268248b78c481daa7e5dc437784003494f | |||
| token_salt: QuSiZr1byZ | |||
| token_last_eight: e4efbf36 | |||
| created_unix: 946687980 | |||
| updated_unix: 946687980 | |||
| @@ -10,7 +13,10 @@ | |||
| id: 2 | |||
| uid: 1 | |||
| name: Token B | |||
| sha1: hash2 | |||
| #token: 4c6f36e6cf498e2a448662f915d932c09c5a146c | |||
| token_hash: 1a0e32a231ebbd582dc626c1543a42d3c63d4fa76c07c72862721467c55e8f81c923d60700f0528b5f5f443f055559d3a279 | |||
| token_salt: Lfwopukrq5 | |||
| token_last_eight: 9c5a146c | |||
| created_unix: 946687980 | |||
| updated_unix: 946687980 | |||
| @@ -18,6 +24,10 @@ | |||
| id: 3 | |||
| uid: 2 | |||
| name: Token A | |||
| sha1: hash3 | |||
| #token: 90a18faa671dc43924b795806ffe4fd169d28c91 | |||
| token_hash: d6d404048048812d9e911d93aefbe94fc768d4876fdf75e3bef0bdc67828e0af422846d3056f2f25ec35c51dc92075685ec5 | |||
| token_salt: 99ArgXKlQQ | |||
| token_last_eight: 69d28c91 | |||
| created_unix: 946687980 | |||
| updated_unix: 946687980 | |||
| #commented out tokens so you can see what they are in plaintext | |||
| @@ -223,6 +223,8 @@ var migrations = []Migration{ | |||
| NewMigration("add uploader id for table attachment", addUploaderIDForAttachment), | |||
| // v84 -> v85 | |||
| NewMigration("add table to store original imported gpg keys", addGPGKeyImport), | |||
| // v85 -> v86 | |||
| NewMigration("hash application token", hashAppToken), | |||
| } | |||
| // Migrate database to current version | |||
| @@ -0,0 +1,135 @@ | |||
| // Copyright 2019 The Gitea Authors. All rights reserved. | |||
| // Use of this source code is governed by a MIT-style | |||
| // license that can be found in the LICENSE file. | |||
| package migrations | |||
| import ( | |||
| "fmt" | |||
| "github.com/go-xorm/core" | |||
| "github.com/go-xorm/xorm" | |||
| "code.gitea.io/gitea/models" | |||
| "code.gitea.io/gitea/modules/generate" | |||
| "code.gitea.io/gitea/modules/log" | |||
| "code.gitea.io/gitea/modules/util" | |||
| ) | |||
| func hashAppToken(x *xorm.Engine) error { | |||
| // AccessToken see models/token.go | |||
| type AccessToken struct { | |||
| ID int64 `xorm:"pk autoincr"` | |||
| UID int64 `xorm:"INDEX"` | |||
| Name string | |||
| Sha1 string | |||
| Token string `xorm:"-"` | |||
| TokenHash string `xorm:"UNIQUE"` // sha256 of token | |||
| TokenSalt string | |||
| TokenLastEight string `xorm:"token_last_eight"` | |||
| CreatedUnix util.TimeStamp `xorm:"INDEX created"` | |||
| UpdatedUnix util.TimeStamp `xorm:"INDEX updated"` | |||
| HasRecentActivity bool `xorm:"-"` | |||
| HasUsed bool `xorm:"-"` | |||
| } | |||
| // First remove the index | |||
| sess := x.NewSession() | |||
| defer sess.Close() | |||
| if err := sess.Begin(); err != nil { | |||
| return err | |||
| } | |||
| var err error | |||
| if models.DbCfg.Type == core.POSTGRES || models.DbCfg.Type == core.SQLITE { | |||
| _, err = sess.Exec("DROP INDEX IF EXISTS UQE_access_token_sha1") | |||
| } else if models.DbCfg.Type == core.MSSQL { | |||
| _, err = sess.Exec(`DECLARE @ConstraintName VARCHAR(256) | |||
| DECLARE @SQL NVARCHAR(256) | |||
| SELECT @ConstraintName = obj.name FROM sys.columns col LEFT OUTER JOIN sys.objects obj ON obj.object_id = col.default_object_id AND obj.type = 'D' WHERE col.object_id = OBJECT_ID('access_token') AND obj.name IS NOT NULL AND col.name = 'sha1' | |||
| SET @SQL = N'ALTER TABLE [access_token] DROP CONSTRAINT [' + @ConstraintName + N']' | |||
| EXEC sp_executesql @SQL`) | |||
| } else if models.DbCfg.Type == core.MYSQL { | |||
| indexes, err := sess.QueryString(`SHOW INDEX FROM access_token WHERE KEY_NAME = 'UQE_access_token_sha1'`) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| if len(indexes) >= 1 { | |||
| _, err = sess.Exec("DROP INDEX UQE_access_token_sha1 ON access_token") | |||
| } | |||
| } else { | |||
| _, err = sess.Exec("DROP INDEX UQE_access_token_sha1 ON access_token") | |||
| } | |||
| if err != nil { | |||
| return fmt.Errorf("Drop index failed: %v", err) | |||
| } | |||
| if err = sess.Commit(); err != nil { | |||
| return err | |||
| } | |||
| if err := sess.Begin(); err != nil { | |||
| return err | |||
| } | |||
| if err := x.Sync2(new(AccessToken)); err != nil { | |||
| return fmt.Errorf("Sync2: %v", err) | |||
| } | |||
| if err = sess.Commit(); err != nil { | |||
| return err | |||
| } | |||
| if err := sess.Begin(); err != nil { | |||
| return err | |||
| } | |||
| // transform all tokens to hashes | |||
| const batchSize = 100 | |||
| for start := 0; ; start += batchSize { | |||
| tokens := make([]*AccessToken, 0, batchSize) | |||
| if err := sess.Limit(batchSize, start).Find(&tokens); err != nil { | |||
| return err | |||
| } | |||
| if len(tokens) == 0 { | |||
| break | |||
| } | |||
| for _, token := range tokens { | |||
| // generate salt | |||
| salt, err := generate.GetRandomString(10) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| token.TokenSalt = salt | |||
| token.TokenHash = hashToken(token.Sha1, salt) | |||
| if len(token.Sha1) < 8 { | |||
| log.Warn("Unable to transform token %s with name %s belonging to user ID %d, skipping transformation", token.Sha1, token.Name, token.UID) | |||
| continue | |||
| } | |||
| token.TokenLastEight = token.Sha1[len(token.Sha1)-8:] | |||
| token.Sha1 = "" // ensure to blank out column in case drop column doesn't work | |||
| if _, err := sess.ID(token.ID).Cols("token_hash, token_salt, token_last_eight, sha1").Update(token); err != nil { | |||
| return fmt.Errorf("couldn't add in sha1, token_hash, token_salt and token_last_eight: %v", err) | |||
| } | |||
| } | |||
| } | |||
| // Commit and begin new transaction for dropping columns | |||
| if err := sess.Commit(); err != nil { | |||
| return err | |||
| } | |||
| if err := sess.Begin(); err != nil { | |||
| return err | |||
| } | |||
| if err := dropTableColumns(sess, "access_token", "sha1"); err != nil { | |||
| return err | |||
| } | |||
| return sess.Commit() | |||
| } | |||
| @@ -1,24 +1,30 @@ | |||
| // Copyright 2014 The Gogs Authors. All rights reserved. | |||
| // Copyright 2019 The Gitea Authors. All rights reserved. | |||
| // Use of this source code is governed by a MIT-style | |||
| // license that can be found in the LICENSE file. | |||
| package models | |||
| import ( | |||
| "crypto/subtle" | |||
| "time" | |||
| gouuid "github.com/satori/go.uuid" | |||
| "code.gitea.io/gitea/modules/base" | |||
| "code.gitea.io/gitea/modules/generate" | |||
| "code.gitea.io/gitea/modules/util" | |||
| ) | |||
| // AccessToken represents a personal access token. | |||
| type AccessToken struct { | |||
| ID int64 `xorm:"pk autoincr"` | |||
| UID int64 `xorm:"INDEX"` | |||
| Name string | |||
| Sha1 string `xorm:"UNIQUE VARCHAR(40)"` | |||
| ID int64 `xorm:"pk autoincr"` | |||
| UID int64 `xorm:"INDEX"` | |||
| Name string | |||
| Token string `xorm:"-"` | |||
| TokenHash string `xorm:"UNIQUE"` // sha256 of token | |||
| TokenSalt string | |||
| TokenLastEight string `xorm:"token_last_eight"` | |||
| CreatedUnix util.TimeStamp `xorm:"INDEX created"` | |||
| UpdatedUnix util.TimeStamp `xorm:"INDEX updated"` | |||
| @@ -34,24 +40,41 @@ func (t *AccessToken) AfterLoad() { | |||
| // NewAccessToken creates new access token. | |||
| func NewAccessToken(t *AccessToken) error { | |||
| t.Sha1 = base.EncodeSha1(gouuid.NewV4().String()) | |||
| _, err := x.Insert(t) | |||
| salt, err := generate.GetRandomString(10) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| t.TokenSalt = salt | |||
| t.Token = base.EncodeSha1(gouuid.NewV4().String()) | |||
| t.TokenHash = hashToken(t.Token, t.TokenSalt) | |||
| t.TokenLastEight = t.Token[len(t.Token)-8:] | |||
| _, err = x.Insert(t) | |||
| return err | |||
| } | |||
| // GetAccessTokenBySHA returns access token by given sha1. | |||
| func GetAccessTokenBySHA(sha string) (*AccessToken, error) { | |||
| if sha == "" { | |||
| // GetAccessTokenBySHA returns access token by given token value | |||
| func GetAccessTokenBySHA(token string) (*AccessToken, error) { | |||
| if token == "" { | |||
| return nil, ErrAccessTokenEmpty{} | |||
| } | |||
| t := &AccessToken{Sha1: sha} | |||
| has, err := x.Get(t) | |||
| if len(token) < 8 { | |||
| return nil, ErrAccessTokenNotExist{token} | |||
| } | |||
| var tokens []AccessToken | |||
| lastEight := token[len(token)-8:] | |||
| err := x.Table(&AccessToken{}).Where("token_last_eight = ?", lastEight).Find(&tokens) | |||
| if err != nil { | |||
| return nil, err | |||
| } else if !has { | |||
| return nil, ErrAccessTokenNotExist{sha} | |||
| } else if len(tokens) == 0 { | |||
| return nil, ErrAccessTokenNotExist{token} | |||
| } | |||
| for _, t := range tokens { | |||
| tempHash := hashToken(token, t.TokenSalt) | |||
| if subtle.ConstantTimeCompare([]byte(t.TokenHash), []byte(tempHash)) == 1 { | |||
| return &t, nil | |||
| } | |||
| } | |||
| return t, nil | |||
| return nil, ErrAccessTokenNotExist{token} | |||
| } | |||
| // ListAccessTokens returns a list of access tokens belongs to given user. | |||
| @@ -29,11 +29,12 @@ func TestNewAccessToken(t *testing.T) { | |||
| func TestGetAccessTokenBySHA(t *testing.T) { | |||
| assert.NoError(t, PrepareTestDatabase()) | |||
| token, err := GetAccessTokenBySHA("hash1") | |||
| token, err := GetAccessTokenBySHA("d2c6c1ba3890b309189a8e618c72a162e4efbf36") | |||
| assert.NoError(t, err) | |||
| assert.Equal(t, int64(1), token.UID) | |||
| assert.Equal(t, "Token A", token.Name) | |||
| assert.Equal(t, "hash1", token.Sha1) | |||
| assert.Equal(t, "2b3668e11cb82d3af8c6e4524fc7841297668f5008d1626f0ad3417e9fa39af84c268248b78c481daa7e5dc437784003494f", token.TokenHash) | |||
| assert.Equal(t, "e4efbf36", token.TokenLastEight) | |||
| token, err = GetAccessTokenBySHA("notahash") | |||
| assert.Error(t, err) | |||
| @@ -69,7 +70,7 @@ func TestListAccessTokens(t *testing.T) { | |||
| func TestUpdateAccessToken(t *testing.T) { | |||
| assert.NoError(t, PrepareTestDatabase()) | |||
| token, err := GetAccessTokenBySHA("hash2") | |||
| token, err := GetAccessTokenBySHA("4c6f36e6cf498e2a448662f915d932c09c5a146c") | |||
| assert.NoError(t, err) | |||
| token.Name = "Token Z" | |||
| @@ -80,7 +81,7 @@ func TestUpdateAccessToken(t *testing.T) { | |||
| func TestDeleteAccessTokenByID(t *testing.T) { | |||
| assert.NoError(t, PrepareTestDatabase()) | |||
| token, err := GetAccessTokenBySHA("hash2") | |||
| token, err := GetAccessTokenBySHA("4c6f36e6cf498e2a448662f915d932c09c5a146c") | |||
| assert.NoError(t, err) | |||
| assert.Equal(t, int64(1), token.UID) | |||
| @@ -9,6 +9,7 @@ import ( | |||
| "crypto/md5" | |||
| "crypto/rand" | |||
| "crypto/sha1" | |||
| "crypto/sha256" | |||
| "encoding/base64" | |||
| "encoding/hex" | |||
| "fmt" | |||
| @@ -54,6 +55,13 @@ func EncodeSha1(str string) string { | |||
| return hex.EncodeToString(h.Sum(nil)) | |||
| } | |||
| // EncodeSha256 string to sha1 hex value. | |||
| func EncodeSha256(str string) string { | |||
| h := sha256.New() | |||
| h.Write([]byte(str)) | |||
| return hex.EncodeToString(h.Sum(nil)) | |||
| } | |||
| // ShortSha is basically just truncating. | |||
| // It is DEPRECATED and will be removed in the future. | |||
| func ShortSha(sha1 string) string { | |||
| @@ -53,6 +53,13 @@ func TestEncodeSha1(t *testing.T) { | |||
| ) | |||
| } | |||
| func TestEncodeSha256(t *testing.T) { | |||
| assert.Equal(t, | |||
| "c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2", | |||
| EncodeSha256("foobar"), | |||
| ) | |||
| } | |||
| func TestShortSha(t *testing.T) { | |||
| assert.Equal(t, "veryverylo", ShortSha("veryverylong")) | |||
| } | |||
| @@ -37,9 +37,9 @@ func ListAccessTokens(ctx *context.APIContext) { | |||
| apiTokens := make([]*api.AccessToken, len(tokens)) | |||
| for i := range tokens { | |||
| apiTokens[i] = &api.AccessToken{ | |||
| ID: tokens[i].ID, | |||
| Name: tokens[i].Name, | |||
| Sha1: tokens[i].Sha1, | |||
| ID: tokens[i].ID, | |||
| Name: tokens[i].Name, | |||
| TokenLastEight: tokens[i].TokenLastEight, | |||
| } | |||
| } | |||
| ctx.JSON(200, &apiTokens) | |||
| @@ -81,9 +81,9 @@ func CreateAccessToken(ctx *context.APIContext, form api.CreateAccessTokenOption | |||
| return | |||
| } | |||
| ctx.JSON(201, &api.AccessToken{ | |||
| Name: t.Name, | |||
| Sha1: t.Sha1, | |||
| ID: t.ID, | |||
| Name: t.Name, | |||
| Token: t.Token, | |||
| ID: t.ID, | |||
| }) | |||
| } | |||
| @@ -49,7 +49,7 @@ func ApplicationsPost(ctx *context.Context, form auth.NewAccessTokenForm) { | |||
| } | |||
| ctx.Flash.Success(ctx.Tr("settings.generate_token_success")) | |||
| ctx.Flash.Info(t.Sha1) | |||
| ctx.Flash.Info(t.Token) | |||
| ctx.Redirect(setting.AppSubURL + "/user/settings/applications") | |||
| } | |||
| @@ -9458,8 +9458,11 @@ | |||
| }, | |||
| "responses": { | |||
| "AccessToken": { | |||
| "description": "AccessToken represents a API access token.", | |||
| "description": "AccessToken represents an API access token.", | |||
| "headers": { | |||
| "hashed_token": { | |||
| "type": "string" | |||
| }, | |||
| "id": { | |||
| "type": "integer", | |||
| "format": "int64" | |||
| @@ -9467,7 +9470,10 @@ | |||
| "name": { | |||
| "type": "string" | |||
| }, | |||
| "sha1": { | |||
| "token": { | |||
| "type": "string" | |||
| }, | |||
| "token_last_eight": { | |||
| "type": "string" | |||
| } | |||
| } | |||
| @@ -1,4 +1,5 @@ | |||
| // Copyright 2014 The Gogs Authors. All rights reserved. | |||
| // Copyright 2019 The Gitea Authors. All rights reserved. | |||
| // Use of this source code is governed by a MIT-style | |||
| // license that can be found in the LICENSE file. | |||
| @@ -17,12 +18,14 @@ func BasicAuthEncode(user, pass string) string { | |||
| return base64.StdEncoding.EncodeToString([]byte(user + ":" + pass)) | |||
| } | |||
| // AccessToken represents a API access token. | |||
| // AccessToken represents an API access token. | |||
| // swagger:response AccessToken | |||
| type AccessToken struct { | |||
| ID int64 `json:"id"` | |||
| Name string `json:"name"` | |||
| Sha1 string `json:"sha1"` | |||
| ID int64 `json:"id"` | |||
| Name string `json:"name"` | |||
| Token string `json:"token"` | |||
| HashedToken string `json:"hashed_token"` | |||
| TokenLastEight string `json:"token_last_eight"` | |||
| } | |||
| // AccessTokenList represents a list of API access token. | |||
| @@ -1,4 +1,4 @@ | |||
| # code.gitea.io/sdk v0.0.0-20190416172854-7d954d775498 | |||
| # code.gitea.io/sdk v0.0.0-20190419065346-2858b80da5f7 | |||
| code.gitea.io/sdk/gitea | |||
| # github.com/BurntSushi/toml v0.3.1 | |||
| github.com/BurntSushi/toml | |||