diff --git a/models/publickey.go b/models/publickey.go index ba15ca45..566814e8 100644 --- a/models/publickey.go +++ b/models/publickey.go @@ -6,6 +6,8 @@ package models import ( "bufio" + "encoding/base64" + "encoding/binary" "errors" "fmt" "io" @@ -111,6 +113,85 @@ var ( } ) +func extractTypeFromBase64Key(key string) (string, error) { + b, err := base64.StdEncoding.DecodeString(key) + if err != nil || len(b) < 4 { + return "", errors.New("Invalid key format") + } + + keyLength := int(binary.BigEndian.Uint32(b)) + + if len(b) < 4+keyLength { + return "", errors.New("Invalid key format") + } + + return string(b[4 : 4+keyLength]), nil +} + +// Parse any key string in openssh or ssh2 format to clean openssh string (rfc4253) +func ParseKeyString(content string) (string, error) { + + // Transform all legal line endings to a single "\n" + s := strings.Replace(strings.Replace(strings.TrimSpace(content), "\r\n", "\n", -1), "\r", "\n", -1) + + lines := strings.Split(s, "\n") + + var keyType, keyContent, keyComment string + + if len(lines) == 1 { + // Parse openssh format + parts := strings.Fields(lines[0]) + switch len(parts) { + case 0: + return "", errors.New("Empty key") + case 1: + keyContent = parts[0] + case 2: + keyType = parts[0] + keyContent = parts[1] + default: + keyType = parts[0] + keyContent = parts[1] + keyComment = parts[2] + } + + // If keyType is not given, extract it from content. If given, validate it + if len(keyType) == 0 { + if t, err := extractTypeFromBase64Key(keyContent); err == nil { + keyType = t + } else { + return "", err + } + } else { + if t, err := extractTypeFromBase64Key(keyContent); err != nil || keyType != t { + return "", err + } + } + } else { + // Parse SSH2 file format. + continuationLine := false + + for _, line := range lines { + // Skip lines that: + // 1) are a continuation of the previous line, + // 2) contain ":" as that are comment lines + // 3) contain "-" as that are begin and end tags + if continuationLine || strings.ContainsAny(line, ":-") { + continuationLine = strings.HasSuffix(line, "\\") + } else { + keyContent = keyContent + line + } + } + + if t, err := extractTypeFromBase64Key(keyContent); err == nil { + keyType = t + } else { + return "", err + } + } + return keyType + " " + keyContent + " " + keyComment, nil +} + // CheckPublicKeyString checks if the given public key string is recognized by SSH. func CheckPublicKeyString(content string) (bool, error) { content = strings.TrimRight(content, "\n\r") diff --git a/routers/user/setting.go b/routers/user/setting.go index 419e84b3..953e6113 100644 --- a/routers/user/setting.go +++ b/routers/user/setting.go @@ -325,10 +325,15 @@ func SettingsSSHKeysPost(ctx *middleware.Context, form auth.AddSSHKeyForm) { return } - // Remove newline characters from form.KeyContent - cleanContent := strings.Replace(form.Content, "\n", "", -1) + // Parse openssh style string from form content + content, err := models.ParseKeyString(form.Content) + if err != nil { + ctx.Flash.Error(ctx.Tr("form.invalid_ssh_key", err.Error())) + ctx.Redirect(setting.AppSubUrl + "/user/settings/ssh") + return + } - if ok, err := models.CheckPublicKeyString(cleanContent); !ok { + if ok, err := models.CheckPublicKeyString(content); !ok { if err == models.ErrKeyUnableVerify { ctx.Flash.Info(ctx.Tr("form.unable_verify_ssh_key")) } else { @@ -341,7 +346,7 @@ func SettingsSSHKeysPost(ctx *middleware.Context, form auth.AddSSHKeyForm) { k := &models.PublicKey{ OwnerId: ctx.User.Id, Name: form.SSHTitle, - Content: cleanContent, + Content: content, } if err := models.AddPublicKey(k); err != nil { if err == models.ErrKeyAlreadyExist {