[#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:
parent
77978b391f
commit
2f0dcd1ac2
6 changed files with 170 additions and 18 deletions
16
README.rst
16
README.rst
|
@ -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
|
||||
=======
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
except pyicloud.exceptions.PyiCloudFailedLoginException:
|
||||
raise RuntimeError('Bad username or password')
|
||||
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:
|
||||
# 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 (
|
||||
|
|
|
@ -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
63
pyicloud/utils.py
Normal 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,
|
||||
)
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue