[#64] Adds Keychain-based Authentication

Squashed commit of the following:

commit 0eb23aa87c264152716933e03827f040742e6d70
Author: Adam Coddington <me@adamcoddington.net>
Date:   Sat Feb 20 14:21:48 2016 -0800

    Updating readme to reflect updated flow.

commit 840268e2db6093b5cb573c6a3e71204bf5b08b48
Author: Adam Coddington <me@adamcoddington.net>
Date:   Sat Feb 20 14:18:39 2016 -0800

    Dropping python 2.6 support workaround.

commit 9dcbd460482c2925bda490be2be884a2a2526062
Author: Adam Coddington <me@adamcoddington.net>
Date:   Sat Feb 20 14:18:00 2016 -0800

    Adding additional behavior at @torarnv's request.

commit 6c711bb12beea7c792b5d386203373423b6e56e2
Author: Adam Coddington <me@adamcoddington.net>
Date:   Sat Jan 23 15:08:29 2016 -0800

    Workaround for obsolete versions of Python 2.

commit b0765b7b6bf9974348061043da9a110c6bd7d985
Author: Adam Coddington <me@adamcoddington.net>
Date:   Sat Jan 23 14:56:53 2016 -0800

    Style changes to avoid line length overage.

commit 4decc576432ef23edae01b9621f2689b4f3c6c84
Author: Adam Coddington <me@adamcoddington.net>
Date:   Sat Jan 23 14:01:27 2016 -0800

    Adding documentation; also adding --delete-from-keyring command-line option.

commit a6b0224e93a8bc9159cf06ba5792a384f7fbb060
Author: Adam Coddington <me@adamcoddington.net>
Date:   Sat Jan 23 13:44:09 2016 -0800

    Adding functionality allowing authentication using iCloud passwords stored in the system keychain.

    Adds the following new command-line options:

    * `--password-interactive`: Allows you to specify your password
      interactively rather than typing it into the command-line.
    * `--store-in-keychain`: Allows you to store the password in use in the
      system keychain.

    If no password is specified when instantiating `PyiCloudService` or when
    using the command-line utility (via either `--password-interactive` or
    `--password`), the system keychain will be queried for a stored
    password, and an exception will be raised if one was not found.

commit 4ba03fb02d51673dfb7183dde49ab4c0bec4afb3
Author: Adam Coddington <me@adamcoddington.net>
Date:   Sat Jan 23 13:43:39 2016 -0800

    Removing unused imports.
This commit is contained in:
Adam Coddington 2016-02-23 17:44:03 -08:00
parent 77978b391f
commit 2f0dcd1ac2
6 changed files with 170 additions and 18 deletions

View file

@ -15,13 +15,27 @@ At its core, PyiCloud connects to iCloud using your username and password, then
Authentication
==============
Authentication is as simple as passing your username and password to the ``PyiCloudService`` class:
Authentication without using a saved password is as simple as passing your username and password to the ``PyiCloudService`` class:
>>> from pyicloud import PyiCloudService
>>> api = PyiCloudService('jappleseed@apple.com', 'password')
In the event that the username/password combination is invalid, a ``PyiCloudFailedLoginException`` exception is thrown.
You can also store your password in the system keyring using the command-line tool:
>>> icloud --username=jappleseed@apple.com
ICloud Password for jappleseed@apple.com:
Save password in keyring? (y/N)
If you have stored a password in the keyring, you will not be required to provide a password when interacting with the command-line tool or instantiating the ``PyiCloudService`` class for the username you stored the password for.
>>> api = PyiCloudService('jappleseed@apple.com')
If you would like to delete a password stored in your system keyring, you can clear a stored password using the ``--delete-from-keyring`` command-line option:
>>> icloud --username=jappleseed@apple.com --delete-from-keyring
=======
Devices
=======

View file

@ -1,10 +1,8 @@
import six
import copy
import uuid
import hashlib
import json
import logging
import pickle
import requests
import sys
import tempfile
@ -19,6 +17,7 @@ from pyicloud.services import (
ContactsService,
RemindersService
)
from pyicloud.utils import get_password_from_keyring
if six.PY3:
import http.cookiejar as cookielib
@ -39,7 +38,12 @@ class PyiCloudService(object):
pyicloud = PyiCloudService('username@apple.com', 'password')
pyicloud.iphone.location()
"""
def __init__(self, apple_id, password, cookie_directory=None, verify=True):
def __init__(
self, apple_id, password=None, cookie_directory=None, verify=True
):
if password is None:
password = get_password_from_keyring(apple_id)
self.discovery = None
self.client_id = str(uuid.uuid1()).upper()
self.user = {'apple_id': apple_id, 'password': password}

View file

@ -9,7 +9,10 @@ import argparse
import pickle
import sys
from click import confirm
import pyicloud
from . import utils
DEVICE_ERROR = (
@ -52,7 +55,25 @@ def main(args=None):
action="store",
dest="password",
default="",
help="Apple ID Password to Use",
help=(
"Apple ID Password to Use; if unspecified, password will be "
"fetched from the system keyring."
)
)
parser.add_argument(
"-n",
"--non-interactive",
action="store_false",
dest="interactive",
default=True,
help="Disable interactive prompts."
)
parser.add_argument(
"--delete-from-keyring",
action="store_true",
dest="delete_from_keyring",
default=False,
help="Delete stored password in system keyring for this username.",
)
parser.add_argument(
"--list",
@ -152,17 +173,57 @@ def main(args=None):
)
command_line = parser.parse_args(args)
if not command_line.username or not command_line.password:
parser.error('No username or password supplied')
from pyicloud import PyiCloudService
try:
api = PyiCloudService(
command_line.username.strip(),
command_line.password.strip()
username = command_line.username
password = command_line.password
if username and command_line.delete_from_keyring:
utils.delete_password_in_keyring(username)
failure_count = 0
while True:
# Which password we use is determined by your username, so we
# do need to check for this first and separately.
if not username:
parser.error('No username supplied')
if not password:
password = utils.get_password(
username,
interactive=command_line.interactive
)
if not password:
parser.error('No password supplied')
try:
api = pyicloud.PyiCloudService(
username.strip(),
password.strip()
)
if (
not utils.password_exists_in_keyring(username) and
command_line.interactive and
confirm("Save password in keyring? ")
):
utils.store_password_in_keyring(username, password)
break
except pyicloud.exceptions.PyiCloudFailedLoginException:
raise RuntimeError('Bad username or password')
# If they have a stored password; we just used it and
# it did not work; let's delete it if there is one.
if utils.password_exists_in_keyring(username):
utils.delete_password_in_keyring(username)
message = "Bad username or password for {username}".format(
username=username,
)
password = None
failure_count += 1
if failure_count >= 3:
raise RuntimeError(message)
print(message, file=sys.stderr)
for dev in api.devices:
if (

View file

@ -1,8 +1,15 @@
class PyiCloudNoDevicesException(Exception):
class PyiCloudException(Exception):
pass
class PyiCloudFailedLoginException(Exception):
class PyiCloudNoDevicesException(PyiCloudException):
pass
class PyiCloudFailedLoginException(PyiCloudException):
pass
class NoStoredPasswordAvailable(PyiCloudException):
pass

63
pyicloud/utils.py Normal file
View file

@ -0,0 +1,63 @@
import getpass
import keyring
from .exceptions import NoStoredPasswordAvailable
KEYRING_SYSTEM = 'pyicloud://icloud-password'
def get_password(username, interactive=True):
try:
return get_password_from_keyring(username)
except NoStoredPasswordAvailable:
if not interactive:
raise
return getpass.getpass(
'ICloud Password for {username}: '.format(
username=username,
)
)
def password_exists_in_keyring(username):
try:
get_password_from_keyring(username)
except NoStoredPasswordAvailable:
return False
return True
def get_password_from_keyring(username):
result = keyring.get_password(
KEYRING_SYSTEM,
username
)
if result is None:
raise NoStoredPasswordAvailable(
"No pyicloud password for {username} could be found "
"in the system keychain. Use the `--store-in-keyring` "
"command-line option for storing a password for this "
"username.".format(
username=username,
)
)
return result
def store_password_in_keyring(username, password):
return keyring.set_password(
KEYRING_SYSTEM,
username,
password,
)
def delete_password_in_keyring(username):
return keyring.delete_password(
KEYRING_SYSTEM,
username,
)

View file

@ -1,4 +1,7 @@
requests>=1.2
keyring>=8.0,<9.0
keyrings.alt>=1.0,<2.0
click>=6.0,<7.0
six
pytz
certifi