Use AES to encrypt password for target

This commit is contained in:
Tan Jiang 2016-08-03 17:25:24 +08:00
parent fa18ab3a75
commit 318d10186e
11 changed files with 115 additions and 14 deletions

View File

@ -28,6 +28,8 @@ env:
HARBOR_ADMIN: admin HARBOR_ADMIN: admin
HARBOR_ADMIN_PASSWD: Harbor12345 HARBOR_ADMIN_PASSWD: Harbor12345
UI_SECRET: tempString UI_SECRET: tempString
MAX_JOB_WORKERS: 3
SECRET_KEY: 1234567890123456
before_install: before_install:
- sudo ./tests/hostcfg.sh - sudo ./tests/hostcfg.sh

View File

@ -122,7 +122,7 @@ create table replication_target (
name varchar(64), name varchar(64),
url varchar(64), url varchar(64),
username varchar(40), username varchar(40),
password varchar(40), password varchar(128),
/* /*
target_type indicates the type of target registry, target_type indicates the type of target registry,
0 means it's a harbor instance, 0 means it's a harbor instance,

View File

@ -44,6 +44,10 @@ use_compressed_js = on
#Maximum number of job workers in job service #Maximum number of job workers in job service
max_job_workers = 3 max_job_workers = 3
#Secret key for encryption/decryption, its length has to be 16 chars
#**NOTE** if this changes, previously encrypted password will not be decrypted!
secret_key = secretkey1234567
#Determine whether the job service should verify the ssl cert when it connects to a remote registry. #Determine whether the job service should verify the ssl cert when it connects to a remote registry.
#Set this flag to off when the remote registry uses a self-signed or untrusted certificate. #Set this flag to off when the remote registry uses a self-signed or untrusted certificate.
verify_remote_cert = on verify_remote_cert = on

View File

@ -16,6 +16,10 @@ if sys.version_info[:3][0] == 3:
import configparser as ConfigParser import configparser as ConfigParser
import io as StringIO import io as StringIO
def validate(conf):
if len(conf.get("configuration", "secret_key")) != 16:
raise Exception("Error: The length of secret key has to be 16 characters!")
#Read configurations #Read configurations
conf = StringIO.StringIO() conf = StringIO.StringIO()
conf.write("[configuration]\n") conf.write("[configuration]\n")
@ -24,6 +28,8 @@ conf.seek(0, os.SEEK_SET)
rcp = ConfigParser.RawConfigParser() rcp = ConfigParser.RawConfigParser()
rcp.readfp(conf) rcp.readfp(conf)
validate(rcp)
hostname = rcp.get("configuration", "hostname") hostname = rcp.get("configuration", "hostname")
ui_url = rcp.get("configuration", "ui_url_protocol") + "://" + hostname ui_url = rcp.get("configuration", "ui_url_protocol") + "://" + hostname
email_server = rcp.get("configuration", "email_server") email_server = rcp.get("configuration", "email_server")
@ -49,6 +55,7 @@ crt_commonname = rcp.get("configuration", "crt_commonname")
crt_email = rcp.get("configuration", "crt_email") crt_email = rcp.get("configuration", "crt_email")
max_job_workers = rcp.get("configuration", "max_job_workers") max_job_workers = rcp.get("configuration", "max_job_workers")
verify_remote_cert = rcp.get("configuration", "verify_remote_cert") verify_remote_cert = rcp.get("configuration", "verify_remote_cert")
secret_key = rcp.get("configuration", "secret_key")
######## ########
ui_secret = ''.join(random.choice(string.ascii_letters+string.digits) for i in range(16)) ui_secret = ''.join(random.choice(string.ascii_letters+string.digits) for i in range(16))
@ -101,6 +108,7 @@ render(os.path.join(templates_dir, "ui", "env"),
self_registration=self_registration, self_registration=self_registration,
use_compressed_js=use_compressed_js, use_compressed_js=use_compressed_js,
ui_secret=ui_secret, ui_secret=ui_secret,
secret_key=secret_key,
verify_remote_cert=verify_remote_cert) verify_remote_cert=verify_remote_cert)
render(os.path.join(templates_dir, "ui", "app.conf"), render(os.path.join(templates_dir, "ui", "app.conf"),
@ -126,6 +134,7 @@ render(os.path.join(templates_dir, "jobservice", "env"),
db_password=db_password, db_password=db_password,
ui_secret=ui_secret, ui_secret=ui_secret,
max_job_workers=max_job_workers, max_job_workers=max_job_workers,
secret_key=secret_key,
ui_url=ui_url, ui_url=ui_url,
verify_remote_cert=verify_remote_cert) verify_remote_cert=verify_remote_cert)

View File

@ -3,6 +3,7 @@ MYSQL_PORT=3306
MYSQL_USR=root MYSQL_USR=root
MYSQL_PWD=$db_password MYSQL_PWD=$db_password
UI_SECRET=$ui_secret UI_SECRET=$ui_secret
SECRET_KEY=$secret_key
CONFIG_PATH=/etc/jobservice/app.conf CONFIG_PATH=/etc/jobservice/app.conf
REGISTRY_URL=http://registry:5000 REGISTRY_URL=http://registry:5000
VERIFY_REMOTE_CERT=$verify_remote_cert VERIFY_REMOTE_CERT=$verify_remote_cert

View File

@ -12,6 +12,7 @@ AUTH_MODE=$auth_mode
LDAP_URL=$ldap_url LDAP_URL=$ldap_url
LDAP_BASE_DN=$ldap_basedn LDAP_BASE_DN=$ldap_basedn
UI_SECRET=$ui_secret UI_SECRET=$ui_secret
SECRET_KEY=$secret_key
SELF_REGISTRATION=$self_registration SELF_REGISTRATION=$self_registration
USE_COMPRESSED_JS=$use_compressed_js USE_COMPRESSED_JS=$use_compressed_js
LOG_LEVEL=debug LOG_LEVEL=debug

View File

@ -20,6 +20,7 @@ import (
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
"os"
"strconv" "strconv"
"github.com/vmware/harbor/dao" "github.com/vmware/harbor/dao"
@ -34,10 +35,14 @@ import (
// TargetAPI handles request to /api/targets/ping /api/targets/{} // TargetAPI handles request to /api/targets/ping /api/targets/{}
type TargetAPI struct { type TargetAPI struct {
BaseAPI BaseAPI
secretKey string
} }
// Prepare validates the user // Prepare validates the user
func (t *TargetAPI) Prepare() { func (t *TargetAPI) Prepare() {
//TODO:move to config
t.secretKey = os.Getenv("SECRET_KEY")
userID := t.ValidateUser() userID := t.ValidateUser()
isSysAdmin, err := dao.IsAdminRole(userID) isSysAdmin, err := dao.IsAdminRole(userID)
if err != nil { if err != nil {
@ -76,7 +81,7 @@ func (t *TargetAPI) Ping() {
password = target.Password password = target.Password
if len(password) != 0 { if len(password) != 0 {
password, err = utils.ReversibleDecrypt(password) password, err = utils.ReversibleDecrypt(password, t.secretKey)
if err != nil { if err != nil {
log.Errorf("failed to decrypt password: %v", err) log.Errorf("failed to decrypt password: %v", err)
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
@ -136,7 +141,7 @@ func (t *TargetAPI) Get() {
// modify other fields of target he does not need to input the password again. // modify other fields of target he does not need to input the password again.
// The security issue can be fixed by enable https. // The security issue can be fixed by enable https.
if len(target.Password) != 0 { if len(target.Password) != 0 {
pwd, err := utils.ReversibleDecrypt(target.Password) pwd, err := utils.ReversibleDecrypt(target.Password, t.secretKey)
if err != nil { if err != nil {
log.Errorf("failed to decrypt password: %v", err) log.Errorf("failed to decrypt password: %v", err)
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
@ -162,7 +167,7 @@ func (t *TargetAPI) List() {
continue continue
} }
str, err := utils.ReversibleDecrypt(target.Password) str, err := utils.ReversibleDecrypt(target.Password, t.secretKey)
if err != nil { if err != nil {
log.Errorf("failed to decrypt password: %v", err) log.Errorf("failed to decrypt password: %v", err)
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
@ -201,7 +206,11 @@ func (t *TargetAPI) Post() {
} }
if len(target.Password) != 0 { if len(target.Password) != 0 {
target.Password = utils.ReversibleEncrypt(target.Password) target.Password, err = utils.ReversibleEncrypt(target.Password, t.secretKey)
if err != nil {
log.Errorf("failed to encrypt password: %v", err)
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
} }
id, err := dao.AddRepTarget(*target) id, err := dao.AddRepTarget(*target)
@ -275,7 +284,11 @@ func (t *TargetAPI) Put() {
target.ID = id target.ID = id
if len(target.Password) != 0 { if len(target.Password) != 0 {
target.Password = utils.ReversibleEncrypt(target.Password) target.Password, err = utils.ReversibleEncrypt(target.Password, t.secretKey)
if err != nil {
log.Errorf("failed to encrypt password: %v", err)
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
} }
if err := dao.UpdateRepTarget(*target); err != nil { if err := dao.UpdateRepTarget(*target); err != nil {

View File

@ -31,6 +31,7 @@ var localUIURL string
var localRegURL string var localRegURL string
var logDir string var logDir string
var uiSecret string var uiSecret string
var secretKey string
var verifyRemoteCert string var verifyRemoteCert string
func init() { func init() {
@ -86,6 +87,11 @@ func init() {
beego.LoadAppConfig("ini", configPath) beego.LoadAppConfig("ini", configPath)
} }
secretKey = os.Getenv("SECRET_KEY")
if len(secretKey) != 16 {
panic("The length of secretkey has to be 16 characters!")
}
log.Debugf("config: maxJobWorkers: %d", maxJobWorkers) log.Debugf("config: maxJobWorkers: %d", maxJobWorkers)
log.Debugf("config: localUIURL: %s", localUIURL) log.Debugf("config: localUIURL: %s", localUIURL)
log.Debugf("config: localRegURL: %s", localRegURL) log.Debugf("config: localRegURL: %s", localRegURL)
@ -119,6 +125,11 @@ func UISecret() string {
return uiSecret return uiSecret
} }
// SecretKey will return the secret key for encryption/decryption password in target.
func SecretKey() string {
return secretKey
}
// VerifyRemoteCert return the flag to tell jobservice whether or not verify the cert of remote registry // VerifyRemoteCert return the flag to tell jobservice whether or not verify the cert of remote registry
func VerifyRemoteCert() bool { func VerifyRemoteCert() bool {
return verifyRemoteCert != "off" return verifyRemoteCert != "off"

View File

@ -231,7 +231,7 @@ func (sm *SM) Reset(jid int64) error {
pwd := target.Password pwd := target.Password
if len(pwd) != 0 { if len(pwd) != 0 {
pwd, err = uti.ReversibleDecrypt(pwd) pwd, err = uti.ReversibleDecrypt(pwd, config.SecretKey())
if err != nil { if err != nil {
return fmt.Errorf("failed to decrypt password: %v", err) return fmt.Errorf("failed to decrypt password: %v", err)
} }

View File

@ -16,9 +16,14 @@
package utils package utils
import ( import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha1" "crypto/sha1"
"encoding/base64" "encoding/base64"
"errors"
"fmt" "fmt"
"io"
"golang.org/x/crypto/pbkdf2" "golang.org/x/crypto/pbkdf2"
) )
@ -28,13 +33,48 @@ func Encrypt(content string, salt string) string {
return fmt.Sprintf("%x", pbkdf2.Key([]byte(content), []byte(salt), 4096, 16, sha1.New)) return fmt.Sprintf("%x", pbkdf2.Key([]byte(content), []byte(salt), 4096, 16, sha1.New))
} }
// ReversibleEncrypt encrypts the str with base64 // ReversibleEncrypt encrypts the str with aes/base64
func ReversibleEncrypt(str string) string { func ReversibleEncrypt(str, key string) (string, error) {
return base64.StdEncoding.EncodeToString([]byte(str)) keyBytes := []byte(key)
var block cipher.Block
var err error
if block, err = aes.NewCipher(keyBytes); err != nil {
return "", err
}
cipherText := make([]byte, aes.BlockSize+len(str))
iv := cipherText[:aes.BlockSize]
if _, err = io.ReadFull(rand.Reader, iv); err != nil {
return "", err
}
cfb := cipher.NewCFBEncrypter(block, iv)
cfb.XORKeyStream(cipherText[aes.BlockSize:], []byte(str))
encrypted := base64.StdEncoding.EncodeToString(cipherText)
return encrypted, nil
} }
// ReversibleDecrypt decrypts the str with base64 // ReversibleDecrypt decrypts the str with aes/base64
func ReversibleDecrypt(str string) (string, error) { func ReversibleDecrypt(str, key string) (string, error) {
b, err := base64.StdEncoding.DecodeString(str) keyBytes := []byte(key)
return string(b), err var block cipher.Block
var cipherText []byte
var err error
if block, err = aes.NewCipher(keyBytes); err != nil {
return "", err
}
if cipherText, err = base64.StdEncoding.DecodeString(str); err != nil {
return "", err
}
if len(cipherText) < aes.BlockSize {
err = errors.New("cipherText too short")
return "", err
}
iv := cipherText[:aes.BlockSize]
cipherText = cipherText[aes.BlockSize:]
cfb := cipher.NewCFBDecrypter(block, iv)
cfb.XORKeyStream(cipherText, cipherText)
return string(cipherText), nil
} }

View File

@ -61,3 +61,23 @@ func TestParseRepository(t *testing.T) {
t.Errorf("unexpected rest: [%s] != [%s]", rest, "") t.Errorf("unexpected rest: [%s] != [%s]", rest, "")
} }
} }
func TestReversibleEncrypt(t *testing.T) {
password := "password"
key := "1234567890123456"
encrypted, err := ReversibleEncrypt(password, key)
if err != nil {
t.Errorf("Failed to encrypt: %v", err)
}
t.Logf("Encrypted password: %s", encrypted)
if encrypted == password {
t.Errorf("Encrypted password is identical to the original")
}
decrypted, err := ReversibleDecrypt(encrypted, key)
if err != nil {
t.Errorf("Failed to decrypt: %v", err)
}
if decrypted != password {
t.Errorf("decrypted password: %s, is not identical to original", decrypted)
}
}