-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Implement GPG API * Better handle error * Apply review recommendation + simplify database operations * Remove useless comments
- Loading branch information
Showing
36 changed files
with
7,931 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,276 @@ | ||
// Copyright 2017 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 ( | ||
"bytes" | ||
"encoding/base64" | ||
"fmt" | ||
"strings" | ||
"time" | ||
|
||
"github.com/go-xorm/xorm" | ||
"golang.org/x/crypto/openpgp" | ||
"golang.org/x/crypto/openpgp/packet" | ||
) | ||
|
||
// GPGKey represents a GPG key. | ||
type GPGKey struct { | ||
ID int64 `xorm:"pk autoincr"` | ||
OwnerID int64 `xorm:"INDEX NOT NULL"` | ||
KeyID string `xorm:"INDEX TEXT NOT NULL"` | ||
PrimaryKeyID string `xorm:"TEXT"` | ||
Content string `xorm:"TEXT NOT NULL"` | ||
Created time.Time `xorm:"-"` | ||
CreatedUnix int64 | ||
Expired time.Time `xorm:"-"` | ||
ExpiredUnix int64 | ||
Added time.Time `xorm:"-"` | ||
AddedUnix int64 | ||
SubsKey []*GPGKey `xorm:"-"` | ||
Emails []*EmailAddress | ||
This comment has been minimized.
Sorry, something went wrong.
This comment has been minimized.
Sorry, something went wrong.
sapk
Author
Member
|
||
CanSign bool | ||
CanEncryptComms bool | ||
CanEncryptStorage bool | ||
CanCertify bool | ||
} | ||
|
||
// BeforeInsert will be invoked by XORM before inserting a record | ||
func (key *GPGKey) BeforeInsert() { | ||
key.AddedUnix = time.Now().Unix() | ||
key.ExpiredUnix = key.Expired.Unix() | ||
key.CreatedUnix = key.Created.Unix() | ||
} | ||
|
||
// AfterSet is invoked from XORM after setting the value of a field of this object. | ||
func (key *GPGKey) AfterSet(colName string, _ xorm.Cell) { | ||
switch colName { | ||
case "key_id": | ||
x.Where("primary_key_id=?", key.KeyID).Find(&key.SubsKey) | ||
case "added_unix": | ||
key.Added = time.Unix(key.AddedUnix, 0).Local() | ||
case "expired_unix": | ||
key.Expired = time.Unix(key.ExpiredUnix, 0).Local() | ||
case "created_unix": | ||
key.Created = time.Unix(key.CreatedUnix, 0).Local() | ||
} | ||
} | ||
|
||
// ListGPGKeys returns a list of public keys belongs to given user. | ||
func ListGPGKeys(uid int64) ([]*GPGKey, error) { | ||
keys := make([]*GPGKey, 0, 5) | ||
return keys, x.Where("owner_id=? AND primary_key_id=''", uid).Find(&keys) | ||
} | ||
|
||
// GetGPGKeyByID returns public key by given ID. | ||
func GetGPGKeyByID(keyID int64) (*GPGKey, error) { | ||
key := new(GPGKey) | ||
has, err := x.Id(keyID).Get(key) | ||
if err != nil { | ||
return nil, err | ||
} else if !has { | ||
return nil, ErrGPGKeyNotExist{keyID} | ||
} | ||
return key, nil | ||
} | ||
|
||
// checkArmoredGPGKeyString checks if the given key string is a valid GPG armored key. | ||
// The function returns the actual public key on success | ||
func checkArmoredGPGKeyString(content string) (*openpgp.Entity, error) { | ||
list, err := openpgp.ReadArmoredKeyRing(strings.NewReader(content)) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return list[0], nil | ||
} | ||
|
||
//addGPGKey add key and subkeys to database | ||
func addGPGKey(e Engine, key *GPGKey) (err error) { | ||
// Save GPG primary key. | ||
if _, err = e.Insert(key); err != nil { | ||
return err | ||
} | ||
// Save GPG subs key. | ||
for _, subkey := range key.SubsKey { | ||
if err := addGPGKey(e, subkey); err != nil { | ||
return err | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
// AddGPGKey adds new public key to database. | ||
func AddGPGKey(ownerID int64, content string) (*GPGKey, error) { | ||
ekey, err := checkArmoredGPGKeyString(content) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
// Key ID cannot be duplicated. | ||
has, err := x.Where("key_id=?", ekey.PrimaryKey.KeyIdString()). | ||
Get(new(GPGKey)) | ||
if err != nil { | ||
return nil, err | ||
} else if has { | ||
return nil, ErrGPGKeyIDAlreadyUsed{ekey.PrimaryKey.KeyIdString()} | ||
} | ||
|
||
//Get DB session | ||
sess := x.NewSession() | ||
defer sessionRelease(sess) | ||
if err = sess.Begin(); err != nil { | ||
return nil, err | ||
} | ||
|
||
key, err := parseGPGKey(ownerID, ekey) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
if err = addGPGKey(sess, key); err != nil { | ||
return nil, err | ||
} | ||
|
||
return key, sess.Commit() | ||
} | ||
|
||
//base64EncPubKey encode public kay content to base 64 | ||
func base64EncPubKey(pubkey *packet.PublicKey) (string, error) { | ||
var w bytes.Buffer | ||
err := pubkey.Serialize(&w) | ||
if err != nil { | ||
return "", err | ||
} | ||
return base64.StdEncoding.EncodeToString(w.Bytes()), nil | ||
} | ||
|
||
//parseSubGPGKey parse a sub Key | ||
func parseSubGPGKey(ownerID int64, primaryID string, pubkey *packet.PublicKey, expiry time.Time) (*GPGKey, error) { | ||
content, err := base64EncPubKey(pubkey) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return &GPGKey{ | ||
OwnerID: ownerID, | ||
KeyID: pubkey.KeyIdString(), | ||
PrimaryKeyID: primaryID, | ||
Content: content, | ||
Created: pubkey.CreationTime, | ||
Expired: expiry, | ||
CanSign: pubkey.CanSign(), | ||
CanEncryptComms: pubkey.PubKeyAlgo.CanEncrypt(), | ||
CanEncryptStorage: pubkey.PubKeyAlgo.CanEncrypt(), | ||
CanCertify: pubkey.PubKeyAlgo.CanSign(), | ||
}, nil | ||
} | ||
|
||
//parseGPGKey parse a PrimaryKey entity (primary key + subs keys + self-signature) | ||
func parseGPGKey(ownerID int64, e *openpgp.Entity) (*GPGKey, error) { | ||
pubkey := e.PrimaryKey | ||
|
||
//Extract self-sign for expire date based on : https://github.com/golang/crypto/blob/master/openpgp/keys.go#L165 | ||
var selfSig *packet.Signature | ||
for _, ident := range e.Identities { | ||
if selfSig == nil { | ||
selfSig = ident.SelfSignature | ||
} else if ident.SelfSignature.IsPrimaryId != nil && *ident.SelfSignature.IsPrimaryId { | ||
selfSig = ident.SelfSignature | ||
break | ||
} | ||
} | ||
expiry := time.Time{} | ||
if selfSig.KeyLifetimeSecs != nil { | ||
expiry = selfSig.CreationTime.Add(time.Duration(*selfSig.KeyLifetimeSecs) * time.Second) | ||
} | ||
|
||
//Parse Subkeys | ||
subkeys := make([]*GPGKey, len(e.Subkeys)) | ||
for i, k := range e.Subkeys { | ||
subs, err := parseSubGPGKey(ownerID, pubkey.KeyIdString(), k.PublicKey, expiry) | ||
if err != nil { | ||
return nil, err | ||
} | ||
subkeys[i] = subs | ||
} | ||
|
||
//Check emails | ||
userEmails, err := GetEmailAddresses(ownerID) | ||
if err != nil { | ||
return nil, err | ||
} | ||
emails := make([]*EmailAddress, len(e.Identities)) | ||
n := 0 | ||
for _, ident := range e.Identities { | ||
|
||
for _, e := range userEmails { | ||
if e.Email == ident.UserId.Email && e.IsActivated { | ||
emails[n] = e | ||
break | ||
} | ||
} | ||
if emails[n] == nil { | ||
return nil, fmt.Errorf("Failed to found email or is not confirmed : %s", ident.UserId.Email) | ||
} | ||
n++ | ||
} | ||
content, err := base64EncPubKey(pubkey) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return &GPGKey{ | ||
OwnerID: ownerID, | ||
KeyID: pubkey.KeyIdString(), | ||
PrimaryKeyID: "", | ||
Content: content, | ||
Created: pubkey.CreationTime, | ||
Expired: expiry, | ||
Emails: emails, | ||
SubsKey: subkeys, | ||
CanSign: pubkey.CanSign(), | ||
CanEncryptComms: pubkey.PubKeyAlgo.CanEncrypt(), | ||
CanEncryptStorage: pubkey.PubKeyAlgo.CanEncrypt(), | ||
CanCertify: pubkey.PubKeyAlgo.CanSign(), | ||
}, nil | ||
} | ||
|
||
// deleteGPGKey does the actual key deletion | ||
func deleteGPGKey(e *xorm.Session, keyID string) (int64, error) { | ||
if keyID == "" { | ||
return 0, fmt.Errorf("empty KeyId forbidden") //Should never happen but just to be sure | ||
} | ||
return e.Where("key_id=?", keyID).Or("primary_key_id=?", keyID).Delete(new(GPGKey)) | ||
} | ||
|
||
// DeleteGPGKey deletes GPG key information in database. | ||
func DeleteGPGKey(doer *User, id int64) (err error) { | ||
key, err := GetGPGKeyByID(id) | ||
if err != nil { | ||
if IsErrGPGKeyNotExist(err) { | ||
return nil | ||
} | ||
return fmt.Errorf("GetPublicKeyByID: %v", err) | ||
} | ||
|
||
// Check if user has access to delete this key. | ||
if !doer.IsAdmin && doer.ID != key.OwnerID { | ||
return ErrGPGKeyAccessDenied{doer.ID, key.ID} | ||
} | ||
|
||
sess := x.NewSession() | ||
defer sessionRelease(sess) | ||
if err = sess.Begin(); err != nil { | ||
return err | ||
} | ||
|
||
if _, err = deleteGPGKey(sess, key.KeyID); err != nil { | ||
return err | ||
} | ||
|
||
if err = sess.Commit(); err != nil { | ||
return err | ||
} | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
// Copyright 2017 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 ( | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestCheckArmoredGPGKeyString(t *testing.T) { | ||
testGPGArmor := `-----BEGIN PGP PUBLIC KEY BLOCK----- | ||
mQENBFh91QoBCADciaDd7aqegYkn4ZIG7J0p1CRwpqMGjxFroJEMg6M1ZiuEVTRv | ||
z49P4kcr1+98NvFmcNc+x5uJgvPCwr/N8ZW5nqBUs2yrklbFF4MeQomyZJJegP8m | ||
/dsRT3BwIT8YMUtJuCj0iqD9vuKYfjrztcMgC1sYwcE9E9OlA0pWBvUdU2i0TIB1 | ||
vOq6slWGvHHa5l5gPfm09idlVxfH5+I+L1uIMx5ovbiVVU5x2f1AR1T18f0t2TVN | ||
0agFTyuoYE1ATmvJHmMcsfgM1Gpd9hIlr9vlupT2kKTPoNzVzsJsOU6Ku/Lf/bac | ||
mF+TfSbRCtmG7dkYZ4metLj7zG/WkW8IvJARABEBAAG0HUFudG9pbmUgR0lSQVJE | ||
IDxzYXBrQHNhcGsuZnI+iQFUBBMBCAA+FiEEEIOwJg/1vpF1itJ4roJVuKDYKOQF | ||
Alh91QoCGwMFCQPCZwAFCwkIBwIGFQgJCgsCBBYCAwECHgECF4AACgkQroJVuKDY | ||
KORreggAlIkC2QjHP5tb7b0+LksB2JMXdY+UzZBcJxtNmvA7gNQaGvWRrhrbePpa | ||
MKDP+3A4BPDBsWFbbB7N56vQ5tROpmWbNKuFOVER4S1bj0JZV0E+xkDLqt9QwQtQ | ||
ojd7oIZJwDUwdud1PvCza2mjgBqqiFE+twbc3i9xjciCGspMniUul1eQYLxRJ0w+ | ||
sbvSOUnujnq5ByMSz9ij00O6aiPfNQS5oB5AALfpjYZDvWAAljLVrtmlQJWZ6dZo | ||
T/YNwsW2dECPuti8+Nmu5FxPGDTXxdbnRaeJTQ3T6q1oUVAv7yTXBx5NXfXkMa5i | ||
iEayQIH8Joq5Ev5ja/lRGQQhArMQ2bkBDQRYfdUKAQgAv7B3coLSrOQbuTZSlgWE | ||
QeT+7DWbmqE1LAQA1pQPcUPXLBUVd60amZJxF9nzUYcY83ylDi0gUNJS+DJGOXpT | ||
pzX2IOuOMGbtUSeKwg5s9O4SUO7f2yCc3RGaegER5zgESxelmOXG+b/hoNt7JbdU | ||
JtxcnLr91Jw2PBO/Xf0ZKJ01CQG2Yzdrrj6jnrHyx94seHy0i6xH1o0OuvfVMLfN | ||
/Vbb/ZHh6ym2wHNqRX62b0VAbchcJXX/MEehXGknKTkO6dDUd+mhRgWMf9ZGRFWx | ||
ag4qALimkf1FXtAyD0vxFYeyoWUQzrOvUsm2BxIN/986R08fhkBQnp5nz07mrU02 | ||
cQARAQABiQE8BBgBCAAmFiEEEIOwJg/1vpF1itJ4roJVuKDYKOQFAlh91QoCGwwF | ||
CQPCZwAACgkQroJVuKDYKOT32wf/UZqMdPn5OhyhffFzjQx7wolrf92WkF2JkxtH | ||
6c3Htjlt/p5RhtKEeErSrNAxB4pqB7dznHaJXiOdWEZtRVXXjlNHjrokGTesqtKk | ||
lHWtK62/MuyLdr+FdCl68F3ewuT2iu/MDv+D4HPqA47zma9xVgZ9ZNwJOpv3fCOo | ||
RfY66UjGEnfgYifgtI5S84/mp2jaSc9UNvlZB6RSf8cfbJUL74kS2lq+xzSlf0yP | ||
Av844q/BfRuVsJsK1NDNG09LC30B0l3LKBqlrRmRTUMHtgchdX2dY+p7GPOoSzlR | ||
MkM/fdpyc2hY7Dl/+qFmN5MG5yGmMpQcX+RNNR222ibNC1D3wg== | ||
=i9b7 | ||
-----END PGP PUBLIC KEY BLOCK-----` | ||
|
||
key, err := checkArmoredGPGKeyString(testGPGArmor) | ||
assert.Nil(t, err, "Could not parse a valid GPG armored key", key) | ||
//TODO verify value of key | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
This may add the tag
xorm:"json"
.