You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

463 lines
12 KiB

10 years ago
10 years ago
10 years ago
9 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
9 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
  1. // Copyright 2014 The Gogs Authors. All rights reserved.
  2. // Use of this source code is governed by a MIT-style
  3. // license that can be found in the LICENSE file.
  4. package models
  5. import (
  6. "bufio"
  7. "encoding/base64"
  8. "encoding/binary"
  9. "errors"
  10. "fmt"
  11. "io"
  12. "io/ioutil"
  13. "os"
  14. "os/exec"
  15. "path"
  16. "path/filepath"
  17. "strings"
  18. "sync"
  19. "time"
  20. "github.com/Unknwon/com"
  21. "github.com/gogits/gogs/modules/log"
  22. "github.com/gogits/gogs/modules/process"
  23. "github.com/gogits/gogs/modules/setting"
  24. )
  25. const (
  26. // "### autogenerated by gitgos, DO NOT EDIT\n"
  27. _TPL_PUBLICK_KEY = `command="%s serv key-%d --config='%s'",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty %s` + "\n"
  28. )
  29. var (
  30. ErrKeyAlreadyExist = errors.New("Public key already exists")
  31. ErrKeyNotExist = errors.New("Public key does not exist")
  32. ErrKeyUnableVerify = errors.New("Unable to verify public key")
  33. )
  34. var sshOpLocker = sync.Mutex{}
  35. var (
  36. SSHPath string // SSH directory.
  37. appPath string // Execution(binary) path.
  38. )
  39. // exePath returns the executable path.
  40. func exePath() (string, error) {
  41. file, err := exec.LookPath(os.Args[0])
  42. if err != nil {
  43. return "", err
  44. }
  45. return filepath.Abs(file)
  46. }
  47. // homeDir returns the home directory of current user.
  48. func homeDir() string {
  49. home, err := com.HomeDir()
  50. if err != nil {
  51. log.Fatal(4, "Fail to get home directory: %v", err)
  52. }
  53. return home
  54. }
  55. func init() {
  56. var err error
  57. if appPath, err = exePath(); err != nil {
  58. log.Fatal(4, "fail to get app path: %v\n", err)
  59. }
  60. appPath = strings.Replace(appPath, "\\", "/", -1)
  61. // Determine and create .ssh path.
  62. SSHPath = filepath.Join(homeDir(), ".ssh")
  63. if err = os.MkdirAll(SSHPath, 0700); err != nil {
  64. log.Fatal(4, "fail to create '%s': %v", SSHPath, err)
  65. }
  66. }
  67. // PublicKey represents a SSH key.
  68. type PublicKey struct {
  69. Id int64
  70. OwnerId int64 `xorm:"UNIQUE(s) INDEX NOT NULL"`
  71. Name string `xorm:"UNIQUE(s) NOT NULL"`
  72. Fingerprint string `xorm:"INDEX NOT NULL"`
  73. Content string `xorm:"TEXT NOT NULL"`
  74. Created time.Time `xorm:"CREATED"`
  75. Updated time.Time
  76. HasRecentActivity bool `xorm:"-"`
  77. HasUsed bool `xorm:"-"`
  78. }
  79. // OmitEmail returns content of public key but without e-mail address.
  80. func (k *PublicKey) OmitEmail() string {
  81. return strings.Join(strings.Split(k.Content, " ")[:2], " ")
  82. }
  83. // GetAuthorizedString generates and returns formatted public key string for authorized_keys file.
  84. func (key *PublicKey) GetAuthorizedString() string {
  85. return fmt.Sprintf(_TPL_PUBLICK_KEY, appPath, key.Id, setting.CustomConf, key.Content)
  86. }
  87. var minimumKeySizes = map[string]int{
  88. "(ED25519)": 256,
  89. "(ECDSA)": 256,
  90. "(NTRU)": 1087,
  91. "(MCE)": 1702,
  92. "(McE)": 1702,
  93. "(RSA)": 2048,
  94. "(DSA)": 1024,
  95. }
  96. func extractTypeFromBase64Key(key string) (string, error) {
  97. b, err := base64.StdEncoding.DecodeString(key)
  98. if err != nil || len(b) < 4 {
  99. return "", errors.New("Invalid key format")
  100. }
  101. keyLength := int(binary.BigEndian.Uint32(b))
  102. if len(b) < 4+keyLength {
  103. return "", errors.New("Invalid key format")
  104. }
  105. return string(b[4 : 4+keyLength]), nil
  106. }
  107. // Parse any key string in openssh or ssh2 format to clean openssh string (rfc4253)
  108. func ParseKeyString(content string) (string, error) {
  109. // Transform all legal line endings to a single "\n"
  110. s := strings.Replace(strings.Replace(strings.TrimSpace(content), "\r\n", "\n", -1), "\r", "\n", -1)
  111. lines := strings.Split(s, "\n")
  112. var keyType, keyContent, keyComment string
  113. if len(lines) == 1 {
  114. // Parse openssh format
  115. parts := strings.Fields(lines[0])
  116. switch len(parts) {
  117. case 0:
  118. return "", errors.New("Empty key")
  119. case 1:
  120. keyContent = parts[0]
  121. case 2:
  122. keyType = parts[0]
  123. keyContent = parts[1]
  124. default:
  125. keyType = parts[0]
  126. keyContent = parts[1]
  127. keyComment = parts[2]
  128. }
  129. // If keyType is not given, extract it from content. If given, validate it
  130. if len(keyType) == 0 {
  131. if t, err := extractTypeFromBase64Key(keyContent); err == nil {
  132. keyType = t
  133. } else {
  134. return "", err
  135. }
  136. } else {
  137. if t, err := extractTypeFromBase64Key(keyContent); err != nil || keyType != t {
  138. return "", err
  139. }
  140. }
  141. } else {
  142. // Parse SSH2 file format.
  143. continuationLine := false
  144. for _, line := range lines {
  145. // Skip lines that:
  146. // 1) are a continuation of the previous line,
  147. // 2) contain ":" as that are comment lines
  148. // 3) contain "-" as that are begin and end tags
  149. if continuationLine || strings.ContainsAny(line, ":-") {
  150. continuationLine = strings.HasSuffix(line, "\\")
  151. } else {
  152. keyContent = keyContent + line
  153. }
  154. }
  155. if t, err := extractTypeFromBase64Key(keyContent); err == nil {
  156. keyType = t
  157. } else {
  158. return "", err
  159. }
  160. }
  161. return keyType + " " + keyContent + " " + keyComment, nil
  162. }
  163. // CheckPublicKeyString checks if the given public key string is recognized by SSH.
  164. func CheckPublicKeyString(content string) (bool, error) {
  165. content = strings.TrimRight(content, "\n\r")
  166. if strings.ContainsAny(content, "\n\r") {
  167. return false, errors.New("only a single line with a single key please")
  168. }
  169. // write the key to a file…
  170. tmpFile, err := ioutil.TempFile(os.TempDir(), "keytest")
  171. if err != nil {
  172. return false, err
  173. }
  174. tmpPath := tmpFile.Name()
  175. defer os.Remove(tmpPath)
  176. tmpFile.WriteString(content)
  177. tmpFile.Close()
  178. // Check if ssh-keygen recognizes its contents.
  179. stdout, stderr, err := process.Exec("CheckPublicKeyString", "ssh-keygen", "-l", "-f", tmpPath)
  180. if err != nil {
  181. return false, errors.New("ssh-keygen -l -f: " + stderr)
  182. } else if len(stdout) < 2 {
  183. return false, errors.New("ssh-keygen returned not enough output to evaluate the key: " + stdout)
  184. }
  185. // The ssh-keygen in Windows does not print key type, so no need go further.
  186. if setting.IsWindows {
  187. return true, nil
  188. }
  189. fmt.Println(stdout)
  190. sshKeygenOutput := strings.Split(stdout, " ")
  191. if len(sshKeygenOutput) < 4 {
  192. return false, ErrKeyUnableVerify
  193. }
  194. // Check if key type and key size match.
  195. if !setting.Service.DisableMinimumKeySizeCheck {
  196. keySize := com.StrTo(sshKeygenOutput[0]).MustInt()
  197. if keySize == 0 {
  198. return false, errors.New("cannot get key size of the given key")
  199. }
  200. keyType := strings.TrimSpace(sshKeygenOutput[len(sshKeygenOutput)-1])
  201. if minimumKeySize := minimumKeySizes[keyType]; minimumKeySize == 0 {
  202. return false, errors.New("sorry, unrecognized public key type")
  203. } else if keySize < minimumKeySize {
  204. return false, fmt.Errorf("the minimum accepted size of a public key %s is %d", keyType, minimumKeySize)
  205. }
  206. }
  207. return true, nil
  208. }
  209. // saveAuthorizedKeyFile writes SSH key content to authorized_keys file.
  210. func saveAuthorizedKeyFile(keys ...*PublicKey) error {
  211. sshOpLocker.Lock()
  212. defer sshOpLocker.Unlock()
  213. fpath := filepath.Join(SSHPath, "authorized_keys")
  214. f, err := os.OpenFile(fpath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
  215. if err != nil {
  216. return err
  217. }
  218. defer f.Close()
  219. fi, err := f.Stat()
  220. if err != nil {
  221. return err
  222. }
  223. // FIXME: following command does not support in Windows.
  224. if !setting.IsWindows {
  225. // .ssh directory should have mode 700, and authorized_keys file should have mode 600.
  226. if fi.Mode().Perm() > 0600 {
  227. log.Error(4, "authorized_keys file has unusual permission flags: %s - setting to -rw-------", fi.Mode().Perm().String())
  228. if err = f.Chmod(0600); err != nil {
  229. return err
  230. }
  231. }
  232. }
  233. for _, key := range keys {
  234. if _, err = f.WriteString(key.GetAuthorizedString()); err != nil {
  235. return err
  236. }
  237. }
  238. return nil
  239. }
  240. // AddPublicKey adds new public key to database and authorized_keys file.
  241. func AddPublicKey(key *PublicKey) (err error) {
  242. has, err := x.Get(key)
  243. if err != nil {
  244. return err
  245. } else if has {
  246. return ErrKeyAlreadyExist
  247. }
  248. // Calculate fingerprint.
  249. tmpPath := strings.Replace(path.Join(os.TempDir(), fmt.Sprintf("%d", time.Now().Nanosecond()),
  250. "id_rsa.pub"), "\\", "/", -1)
  251. os.MkdirAll(path.Dir(tmpPath), os.ModePerm)
  252. if err = ioutil.WriteFile(tmpPath, []byte(key.Content), os.ModePerm); err != nil {
  253. return err
  254. }
  255. stdout, stderr, err := process.Exec("AddPublicKey", "ssh-keygen", "-l", "-f", tmpPath)
  256. if err != nil {
  257. return errors.New("ssh-keygen -l -f: " + stderr)
  258. } else if len(stdout) < 2 {
  259. return errors.New("not enough output for calculating fingerprint: " + stdout)
  260. }
  261. key.Fingerprint = strings.Split(stdout, " ")[1]
  262. if has, err := x.Get(&PublicKey{Fingerprint: key.Fingerprint}); err == nil && has {
  263. return ErrKeyAlreadyExist
  264. }
  265. // Save SSH key.
  266. if _, err = x.Insert(key); err != nil {
  267. return err
  268. } else if err = saveAuthorizedKeyFile(key); err != nil {
  269. // Roll back.
  270. if _, err2 := x.Delete(key); err2 != nil {
  271. return err2
  272. }
  273. return err
  274. }
  275. return nil
  276. }
  277. // GetPublicKeyById returns public key by given ID.
  278. func GetPublicKeyById(keyId int64) (*PublicKey, error) {
  279. key := new(PublicKey)
  280. has, err := x.Id(keyId).Get(key)
  281. if err != nil {
  282. return nil, err
  283. } else if !has {
  284. return nil, ErrKeyNotExist
  285. }
  286. return key, nil
  287. }
  288. // ListPublicKeys returns a list of public keys belongs to given user.
  289. func ListPublicKeys(uid int64) ([]*PublicKey, error) {
  290. keys := make([]*PublicKey, 0, 5)
  291. err := x.Where("owner_id=?", uid).Find(&keys)
  292. if err != nil {
  293. return nil, err
  294. }
  295. for _, key := range keys {
  296. key.HasUsed = key.Updated.After(key.Created)
  297. key.HasRecentActivity = key.Updated.Add(7 * 24 * time.Hour).After(time.Now())
  298. }
  299. return keys, nil
  300. }
  301. // rewriteAuthorizedKeys finds and deletes corresponding line in authorized_keys file.
  302. func rewriteAuthorizedKeys(key *PublicKey, p, tmpP string) error {
  303. sshOpLocker.Lock()
  304. defer sshOpLocker.Unlock()
  305. fr, err := os.Open(p)
  306. if err != nil {
  307. return err
  308. }
  309. defer fr.Close()
  310. fw, err := os.OpenFile(tmpP, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
  311. if err != nil {
  312. return err
  313. }
  314. defer fw.Close()
  315. isFound := false
  316. keyword := fmt.Sprintf("key-%d", key.Id)
  317. buf := bufio.NewReader(fr)
  318. for {
  319. line, errRead := buf.ReadString('\n')
  320. line = strings.TrimSpace(line)
  321. if errRead != nil {
  322. if errRead != io.EOF {
  323. return errRead
  324. }
  325. // Reached end of file, if nothing to read then break,
  326. // otherwise handle the last line.
  327. if len(line) == 0 {
  328. break
  329. }
  330. }
  331. // Found the line and copy rest of file.
  332. if !isFound && strings.Contains(line, keyword) && strings.Contains(line, key.Content) {
  333. isFound = true
  334. continue
  335. }
  336. // Still finding the line, copy the line that currently read.
  337. if _, err = fw.WriteString(line + "\n"); err != nil {
  338. return err
  339. }
  340. if errRead == io.EOF {
  341. break
  342. }
  343. }
  344. return nil
  345. }
  346. // UpdatePublicKey updates given public key.
  347. func UpdatePublicKey(key *PublicKey) error {
  348. _, err := x.Id(key.Id).AllCols().Update(key)
  349. return err
  350. }
  351. // DeletePublicKey deletes SSH key information both in database and authorized_keys file.
  352. func DeletePublicKey(key *PublicKey) error {
  353. has, err := x.Get(key)
  354. if err != nil {
  355. return err
  356. } else if !has {
  357. return ErrKeyNotExist
  358. }
  359. if _, err = x.Delete(key); err != nil {
  360. return err
  361. }
  362. fpath := filepath.Join(SSHPath, "authorized_keys")
  363. tmpPath := filepath.Join(SSHPath, "authorized_keys.tmp")
  364. if err = rewriteAuthorizedKeys(key, fpath, tmpPath); err != nil {
  365. return err
  366. } else if err = os.Remove(fpath); err != nil {
  367. return err
  368. }
  369. return os.Rename(tmpPath, fpath)
  370. }
  371. // RewriteAllPublicKeys removes any authorized key and rewrite all keys from database again.
  372. func RewriteAllPublicKeys() error {
  373. sshOpLocker.Lock()
  374. defer sshOpLocker.Unlock()
  375. tmpPath := filepath.Join(SSHPath, "authorized_keys.tmp")
  376. f, err := os.OpenFile(tmpPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
  377. if err != nil {
  378. return err
  379. }
  380. defer os.Remove(tmpPath)
  381. err = x.Iterate(new(PublicKey), func(idx int, bean interface{}) (err error) {
  382. _, err = f.WriteString((bean.(*PublicKey)).GetAuthorizedString())
  383. return err
  384. })
  385. f.Close()
  386. if err != nil {
  387. return err
  388. }
  389. fpath := filepath.Join(SSHPath, "authorized_keys")
  390. if com.IsExist(fpath) {
  391. if err = os.Remove(fpath); err != nil {
  392. return err
  393. }
  394. }
  395. if err = os.Rename(tmpPath, fpath); err != nil {
  396. return err
  397. }
  398. return nil
  399. }