diff options
| author | Ted Zlatanov | 2012-07-29 22:07:41 +0000 |
|---|---|---|
| committer | Katsumi Yamaoka | 2012-07-29 22:07:41 +0000 |
| commit | d7fcec5d7d7b5c457dd6b44572f672c81691e171 (patch) | |
| tree | 40bd1eb45cb5d268505a5f09ec420c6d57bc1128 | |
| parent | c91562a619ba72b93196791a519e6481ff633fc1 (diff) | |
| download | emacs-d7fcec5d7d7b5c457dd6b44572f672c81691e171.tar.gz emacs-d7fcec5d7d7b5c457dd6b44572f672c81691e171.zip | |
lisp/gnus/auth-source.el: Support Mac OS X Keychains
| -rw-r--r-- | lisp/gnus/ChangeLog | 11 | ||||
| -rw-r--r-- | lisp/gnus/auth-source.el | 264 |
2 files changed, 274 insertions, 1 deletions
diff --git a/lisp/gnus/ChangeLog b/lisp/gnus/ChangeLog index 0476fc043c2..9426b7889c8 100644 --- a/lisp/gnus/ChangeLog +++ b/lisp/gnus/ChangeLog | |||
| @@ -1,3 +1,14 @@ | |||
| 1 | 2012-07-29 Teodor Zlatanov <tzz@lifelogs.com> | ||
| 2 | |||
| 3 | * auth-source.el (auth-sources, auth-source-backend-parse) | ||
| 4 | (auth-source-macos-keychain-search) | ||
| 5 | (auth-source-macos-keychain-search-items) | ||
| 6 | (auth-source-macos-keychain-result-append) | ||
| 7 | (auth-source-macos-keychain-create): Support Mac OS X Keychains in | ||
| 8 | auth-source.el through the /usr/bin/security utility. | ||
| 9 | (auth-sources): Fix syntax error. | ||
| 10 | (auth-source-macos-keychain-result-append): Fix variable name. | ||
| 11 | |||
| 1 | 2012-07-27 Julien Danjou <jd@dex.adm.naquadah.org> | 12 | 2012-07-27 Julien Danjou <jd@dex.adm.naquadah.org> |
| 2 | 13 | ||
| 3 | * message.el (fboundp): Add a defalias on `mail-dont-reply-to' for | 14 | * message.el (fboundp): Add a defalias on `mail-dont-reply-to' for |
diff --git a/lisp/gnus/auth-source.el b/lisp/gnus/auth-source.el index 47359500dc4..87f09632250 100644 --- a/lisp/gnus/auth-source.el +++ b/lisp/gnus/auth-source.el | |||
| @@ -254,6 +254,13 @@ can get pretty complex." | |||
| 254 | (const :tag "Default Secrets API Collection" 'default) | 254 | (const :tag "Default Secrets API Collection" 'default) |
| 255 | (const :tag "Login Secrets API Collection" "secrets:Login") | 255 | (const :tag "Login Secrets API Collection" "secrets:Login") |
| 256 | (const :tag "Temp Secrets API Collection" "secrets:session") | 256 | (const :tag "Temp Secrets API Collection" "secrets:session") |
| 257 | |||
| 258 | (const :tag "Default internet Mac OS Keychain" | ||
| 259 | 'macos-keychain-internet) | ||
| 260 | |||
| 261 | (const :tag "Default generic Mac OS Keychain" | ||
| 262 | 'macos-keychain-generic) | ||
| 263 | |||
| 257 | (list :tag "Source definition" | 264 | (list :tag "Source definition" |
| 258 | (const :format "" :value :source) | 265 | (const :format "" :value :source) |
| 259 | (choice :tag "Authentication backend choice" | 266 | (choice :tag "Authentication backend choice" |
| @@ -266,7 +273,21 @@ can get pretty complex." | |||
| 266 | (const :tag "Default" 'default) | 273 | (const :tag "Default" 'default) |
| 267 | (const :tag "Login" "Login") | 274 | (const :tag "Login" "Login") |
| 268 | (const | 275 | (const |
| 269 | :tag "Temporary" "session")))) | 276 | :tag "Temporary" "session"))) |
| 277 | (list | ||
| 278 | :tag "Mac OS internet Keychain" | ||
| 279 | (const :format "" | ||
| 280 | :value :macos-keychain-internet) | ||
| 281 | (choice :tag "Collection to use" | ||
| 282 | (string :tag "internet Keychain path") | ||
| 283 | (const :tag "default" 'default)))) | ||
| 284 | (list | ||
| 285 | :tag "Mac OS generic Keychain" | ||
| 286 | (const :format "" | ||
| 287 | :value :macos-keychain-generic) | ||
| 288 | (choice :tag "Collection to use" | ||
| 289 | (string :tag "generic Keychain path") | ||
| 290 | (const :tag "default" 'default)))) | ||
| 270 | (repeat :tag "Extra Parameters" :inline t | 291 | (repeat :tag "Extra Parameters" :inline t |
| 271 | (choice :tag "Extra parameter" | 292 | (choice :tag "Extra parameter" |
| 272 | (list | 293 | (list |
| @@ -377,6 +398,10 @@ with \"[a/b/c] \" if CHOICES is '\(?a ?b ?c\)." | |||
| 377 | ;; (auth-source-backend-parse "myfile.gpg") | 398 | ;; (auth-source-backend-parse "myfile.gpg") |
| 378 | ;; (auth-source-backend-parse 'default) | 399 | ;; (auth-source-backend-parse 'default) |
| 379 | ;; (auth-source-backend-parse "secrets:Login") | 400 | ;; (auth-source-backend-parse "secrets:Login") |
| 401 | ;; (auth-source-backend-parse 'macos-keychain-internet) | ||
| 402 | ;; (auth-source-backend-parse 'macos-keychain-generic) | ||
| 403 | ;; (auth-source-backend-parse "macos-keychain-internet:/path/here.keychain") | ||
| 404 | ;; (auth-source-backend-parse "macos-keychain-generic:/path/here.keychain") | ||
| 380 | 405 | ||
| 381 | (defun auth-source-backend-parse (entry) | 406 | (defun auth-source-backend-parse (entry) |
| 382 | "Creates an auth-source-backend from an ENTRY in `auth-sources'." | 407 | "Creates an auth-source-backend from an ENTRY in `auth-sources'." |
| @@ -391,6 +416,28 @@ with \"[a/b/c] \" if CHOICES is '\(?a ?b ?c\)." | |||
| 391 | ;; matching any user, host, and protocol | 416 | ;; matching any user, host, and protocol |
| 392 | ((and (stringp entry) (string-match "^secrets:\\(.+\\)" entry)) | 417 | ((and (stringp entry) (string-match "^secrets:\\(.+\\)" entry)) |
| 393 | (auth-source-backend-parse `(:source (:secrets ,(match-string 1 entry))))) | 418 | (auth-source-backend-parse `(:source (:secrets ,(match-string 1 entry))))) |
| 419 | |||
| 420 | ;; take 'macos-keychain-internet and recurse to get it as a Mac OS | ||
| 421 | ;; Keychain collection matching any user, host, and protocol | ||
| 422 | ((eq entry 'macos-keychain-internet) | ||
| 423 | (auth-source-backend-parse '(:source (:macos-keychain-internet default)))) | ||
| 424 | ;; take 'macos-keychain-generic and recurse to get it as a Mac OS | ||
| 425 | ;; Keychain collection matching any user, host, and protocol | ||
| 426 | ((eq entry 'macos-keychain-generic) | ||
| 427 | (auth-source-backend-parse '(:source (:macos-keychain-generic default)))) | ||
| 428 | ;; take macos-keychain-internet:XYZ and recurse to get it as MacOS | ||
| 429 | ;; Keychain "XYZ" matching any user, host, and protocol | ||
| 430 | ((and (stringp entry) (string-match "^macos-keychain-internet:\\(.+\\)" | ||
| 431 | entry)) | ||
| 432 | (auth-source-backend-parse `(:source (:macos-keychain-internet | ||
| 433 | ,(match-string 1 entry))))) | ||
| 434 | ;; take macos-keychain-generic:XYZ and recurse to get it as MacOS | ||
| 435 | ;; Keychain "XYZ" matching any user, host, and protocol | ||
| 436 | ((and (stringp entry) (string-match "^macos-keychain-generic:\\(.+\\)" | ||
| 437 | entry)) | ||
| 438 | (auth-source-backend-parse `(:source (:macos-keychain-generic | ||
| 439 | ,(match-string 1 entry))))) | ||
| 440 | |||
| 394 | ;; take just a file name and recurse to get it as a netrc file | 441 | ;; take just a file name and recurse to get it as a netrc file |
| 395 | ;; matching any user, host, and protocol | 442 | ;; matching any user, host, and protocol |
| 396 | ((stringp entry) | 443 | ((stringp entry) |
| @@ -413,6 +460,33 @@ with \"[a/b/c] \" if CHOICES is '\(?a ?b ?c\)." | |||
| 413 | :search-function 'auth-source-netrc-search | 460 | :search-function 'auth-source-netrc-search |
| 414 | :create-function 'auth-source-netrc-create))) | 461 | :create-function 'auth-source-netrc-create))) |
| 415 | 462 | ||
| 463 | ;; the MacOS Keychain | ||
| 464 | ((and | ||
| 465 | (not (null (plist-get entry :source))) ; the source must not be nil | ||
| 466 | (listp (plist-get entry :source)) ; and it must be a list | ||
| 467 | (or | ||
| 468 | (plist-get (plist-get entry :source) :macos-keychain-generic) | ||
| 469 | (plist-get (plist-get entry :source) :macos-keychain-internet))) | ||
| 470 | |||
| 471 | (let* ((source-spec (plist-get entry :source)) | ||
| 472 | (keychain-generic (plist-get source-spec :macos-keychain-generic)) | ||
| 473 | (keychain-type (if keychain-generic | ||
| 474 | 'macos-keychain-generic | ||
| 475 | 'macos-keychain-internet)) | ||
| 476 | (source (plist-get source-spec (if keychain-generic | ||
| 477 | :macos-keychain-generic | ||
| 478 | :macos-keychain-internet)))) | ||
| 479 | |||
| 480 | (when (symbolp source) | ||
| 481 | (setq source (symbol-name source))) | ||
| 482 | |||
| 483 | (auth-source-backend | ||
| 484 | (format "Mac OS Keychain (%s)" source) | ||
| 485 | :source source | ||
| 486 | :type keychain-type | ||
| 487 | :search-function 'auth-source-macos-keychain-search | ||
| 488 | :create-function 'auth-source-macos-keychain-create))) | ||
| 489 | |||
| 416 | ;; the Secrets API. We require the package, in order to have a | 490 | ;; the Secrets API. We require the package, in order to have a |
| 417 | ;; defined value for `secrets-enabled'. | 491 | ;; defined value for `secrets-enabled'. |
| 418 | ((and | 492 | ((and |
| @@ -694,6 +768,7 @@ must call it to obtain the actual value." | |||
| 694 | (let* ((bmatches (apply | 768 | (let* ((bmatches (apply |
| 695 | (slot-value backend 'search-function) | 769 | (slot-value backend 'search-function) |
| 696 | :backend backend | 770 | :backend backend |
| 771 | :type (slot-value backend :type) | ||
| 697 | ;; note we're overriding whatever the spec | 772 | ;; note we're overriding whatever the spec |
| 698 | ;; has for :require, :create, and :delete | 773 | ;; has for :require, :create, and :delete |
| 699 | :require require | 774 | :require require |
| @@ -1515,6 +1590,193 @@ authentication tokens: | |||
| 1515 | ;; (apply 'secrets-create-item (auth-get-source entry) name passwd spec) | 1590 | ;; (apply 'secrets-create-item (auth-get-source entry) name passwd spec) |
| 1516 | (debug spec)) | 1591 | (debug spec)) |
| 1517 | 1592 | ||
| 1593 | ;;; Backend specific parsing: Mac OS Keychain (using /usr/bin/security) backend | ||
| 1594 | |||
| 1595 | ;; (let ((auth-sources '(macos-keychain-internet))) (auth-source-search :max 1 :create t)) | ||
| 1596 | ;; (let ((auth-sources '(macos-keychain-internet))) (auth-source-search :max 1 :delete t)) | ||
| 1597 | ;; (let ((auth-sources '(macos-keychain-internet))) (auth-source-search :max 1)) | ||
| 1598 | ;; (let ((auth-sources '(macos-keychain-internet))) (auth-source-search)) | ||
| 1599 | |||
| 1600 | ;; (let ((auth-sources '(macos-keychain-generic))) (auth-source-search :max 1 :create t)) | ||
| 1601 | ;; (let ((auth-sources '(macos-keychain-generic))) (auth-source-search :max 1 :delete t)) | ||
| 1602 | ;; (let ((auth-sources '(macos-keychain-generic))) (auth-source-search :max 1)) | ||
| 1603 | ;; (let ((auth-sources '(macos-keychain-generic))) (auth-source-search)) | ||
| 1604 | |||
| 1605 | ;; (let ((auth-sources '("macos-keychain-internet:/Users/tzz/Library/Keychains/login.keychain"))) (auth-source-search :max 1)) | ||
| 1606 | ;; (let ((auth-sources '("macos-keychain-generic:Login"))) (auth-source-search :max 1 :host "git.gnus.org")) | ||
| 1607 | |||
| 1608 | (defun* auth-source-macos-keychain-search (&rest | ||
| 1609 | spec | ||
| 1610 | &key backend create delete label | ||
| 1611 | type max host user port | ||
| 1612 | &allow-other-keys) | ||
| 1613 | "Search the MacOS Keychain; spec is like `auth-source'. | ||
| 1614 | |||
| 1615 | All search keys must match exactly. If you need substring | ||
| 1616 | matching, do a wider search and narrow it down yourself. | ||
| 1617 | |||
| 1618 | You'll get back all the properties of the token as a plist. | ||
| 1619 | |||
| 1620 | The :type key is either 'macos-keychain-internet or | ||
| 1621 | 'macos-keychain-generic. | ||
| 1622 | |||
| 1623 | For the internet keychain type, the :label key searches the | ||
| 1624 | item's labels (\"-l LABEL\" passed to \"/usr/bin/security\"). | ||
| 1625 | Similarly, :host maps to \"-s HOST\", :user maps to \"-a USER\", | ||
| 1626 | and :port maps to \"-P PORT\" or \"-r PROT\" | ||
| 1627 | (note PROT has to be a 4-character string). | ||
| 1628 | |||
| 1629 | For the generic keychain type, the :label key searches the item's | ||
| 1630 | labels (\"-l LABEL\" passed to \"/usr/bin/security\"). | ||
| 1631 | Similarly, :host maps to \"-c HOST\" (the \"creator\" keychain | ||
| 1632 | field), :user maps to \"-a USER\", and :port maps to \"-s PORT\". | ||
| 1633 | |||
| 1634 | Here's an example that looks for the first item in the default | ||
| 1635 | generic MacOS Keychain: | ||
| 1636 | |||
| 1637 | \(let ((auth-sources '(macos-keychain-generic))) | ||
| 1638 | (auth-source-search :max 1) | ||
| 1639 | |||
| 1640 | Here's another that looks for the first item in the internet | ||
| 1641 | MacOS Keychain collection whose label is 'gnus': | ||
| 1642 | |||
| 1643 | \(let ((auth-sources '(macos-keychain-internet))) | ||
| 1644 | (auth-source-search :max 1 :label \"gnus\") | ||
| 1645 | |||
| 1646 | And this one looks for the first item in the internet keychain | ||
| 1647 | entries for git.gnus.org: | ||
| 1648 | |||
| 1649 | \(let ((auth-sources '(macos-keychain-internet\"))) | ||
| 1650 | (auth-source-search :max 1 :host \"git.gnus.org\")) | ||
| 1651 | " | ||
| 1652 | ;; TODO | ||
| 1653 | (assert (not create) nil | ||
| 1654 | "The MacOS Keychain auth-source backend doesn't support creation yet") | ||
| 1655 | ;; TODO | ||
| 1656 | ;; (macos-keychain-delete-item coll elt) | ||
| 1657 | (assert (not delete) nil | ||
| 1658 | "The MacOS Keychain auth-source backend doesn't support deletion yet") | ||
| 1659 | |||
| 1660 | (let* ((coll (oref backend source)) | ||
| 1661 | (max (or max 5000)) ; sanity check: default to stop at 5K | ||
| 1662 | (ignored-keys '(:create :delete :max :backend :label)) | ||
| 1663 | (search-keys (loop for i below (length spec) by 2 | ||
| 1664 | unless (memq (nth i spec) ignored-keys) | ||
| 1665 | collect (nth i spec))) | ||
| 1666 | ;; build a search spec without the ignored keys | ||
| 1667 | ;; if a search key is nil or t (match anything), we skip it | ||
| 1668 | (search-spec (apply 'append (mapcar | ||
| 1669 | (lambda (k) | ||
| 1670 | (if (or (null (plist-get spec k)) | ||
| 1671 | (eq t (plist-get spec k))) | ||
| 1672 | nil | ||
| 1673 | (list k (plist-get spec k)))) | ||
| 1674 | search-keys))) | ||
| 1675 | ;; needed keys (always including host, login, port, and secret) | ||
| 1676 | (returned-keys (mm-delete-duplicates (append | ||
| 1677 | '(:host :login :port :secret) | ||
| 1678 | search-keys))) | ||
| 1679 | (items (apply 'auth-source-macos-keychain-search-items | ||
| 1680 | coll | ||
| 1681 | type | ||
| 1682 | max | ||
| 1683 | search-spec)) | ||
| 1684 | |||
| 1685 | ;; ensure each item has each key in `returned-keys' | ||
| 1686 | (items (mapcar (lambda (plist) | ||
| 1687 | (append | ||
| 1688 | (apply 'append | ||
| 1689 | (mapcar (lambda (req) | ||
| 1690 | (if (plist-get plist req) | ||
| 1691 | nil | ||
| 1692 | (list req nil))) | ||
| 1693 | returned-keys)) | ||
| 1694 | plist)) | ||
| 1695 | items))) | ||
| 1696 | items)) | ||
| 1697 | |||
| 1698 | (defun* auth-source-macos-keychain-search-items (coll type max | ||
| 1699 | &rest spec | ||
| 1700 | &key label type | ||
| 1701 | host user port | ||
| 1702 | &allow-other-keys) | ||
| 1703 | |||
| 1704 | (let* ((keychain-generic (eq type 'macos-keychain-generic)) | ||
| 1705 | (args `(,(if keychain-generic | ||
| 1706 | "find-generic-password" | ||
| 1707 | "find-internet-password") | ||
| 1708 | "-g")) | ||
| 1709 | (ret (list :type type))) | ||
| 1710 | (when label | ||
| 1711 | (setq args (append args (list "-l" label)))) | ||
| 1712 | (when host | ||
| 1713 | (setq args (append args (list (if keychain-generic "-c" "-s") host)))) | ||
| 1714 | (when user | ||
| 1715 | (setq args (append args (list "-a" user)))) | ||
| 1716 | |||
| 1717 | (when port | ||
| 1718 | (if keychain-generic | ||
| 1719 | (setq args (append args (list "-s" port))) | ||
| 1720 | (setq args (append args (list | ||
| 1721 | (if (string-match "[0-9]+" port) "-P" "-r") | ||
| 1722 | port))))) | ||
| 1723 | |||
| 1724 | (unless (equal coll "default") | ||
| 1725 | (setq args (append args (list coll)))) | ||
| 1726 | |||
| 1727 | (with-temp-buffer | ||
| 1728 | (apply 'call-process "/usr/bin/security" nil t nil args) | ||
| 1729 | (goto-char (point-min)) | ||
| 1730 | (while (not (eobp)) | ||
| 1731 | (cond | ||
| 1732 | ((looking-at "^password: \"\\(.+\\)\"$") | ||
| 1733 | (auth-source-macos-keychain-result-append | ||
| 1734 | ret | ||
| 1735 | keychain-generic | ||
| 1736 | "secret" | ||
| 1737 | (lexical-let ((v (match-string 1))) | ||
| 1738 | (lambda () v)))) | ||
| 1739 | ;; TODO: check if this is really the label | ||
| 1740 | ;; match 0x00000007 <blob>="AppleID" | ||
| 1741 | ((looking-at "^[ ]+0x00000007 <blob>=\"\\(.+\\)\"") | ||
| 1742 | (auth-source-macos-keychain-result-append | ||
| 1743 | ret | ||
| 1744 | keychain-generic | ||
| 1745 | "label" | ||
| 1746 | (match-string 1))) | ||
| 1747 | ;; match "crtr"<uint32>="aapl" | ||
| 1748 | ;; match "svce"<blob>="AppleID" | ||
| 1749 | ((looking-at "^[ ]+\"\\([a-z]+\\)\"[^=]+=\"\\(.+\\)\"") | ||
| 1750 | (auth-source-macos-keychain-result-append | ||
| 1751 | ret | ||
| 1752 | keychain-generic | ||
| 1753 | (match-string 1) | ||
| 1754 | (match-string 2)))) | ||
| 1755 | (forward-line))) | ||
| 1756 | ;; return `ret' iff it has the :secret key | ||
| 1757 | (and (plist-get ret :secret) (list ret)))) | ||
| 1758 | |||
| 1759 | (defun auth-source-macos-keychain-result-append (result generic k v) | ||
| 1760 | (push v ret) | ||
| 1761 | (setq k (cond | ||
| 1762 | ((equal k "acct") "user") | ||
| 1763 | ;; for generic keychains, creator is host, service is port | ||
| 1764 | ((and generic (equal k "crtr")) "host") | ||
| 1765 | ((and generic (equal k "svce")) "port") | ||
| 1766 | ;; for internet keychains, protocol is port, server is host | ||
| 1767 | ((and (not generic) (equal k "ptcl")) "port") | ||
| 1768 | ((and (not generic) (equal k "srvr")) "host") | ||
| 1769 | (t k))) | ||
| 1770 | |||
| 1771 | (push (intern (format ":%s" k)) ret)) | ||
| 1772 | |||
| 1773 | (defun* auth-source-macos-keychain-create (&rest | ||
| 1774 | spec | ||
| 1775 | &key backend type max host user port | ||
| 1776 | &allow-other-keys) | ||
| 1777 | ;; TODO | ||
| 1778 | (debug spec)) | ||
| 1779 | |||
| 1518 | ;;; Backend specific parsing: PLSTORE backend | 1780 | ;;; Backend specific parsing: PLSTORE backend |
| 1519 | 1781 | ||
| 1520 | (defun* auth-source-plstore-search (&rest | 1782 | (defun* auth-source-plstore-search (&rest |