[#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
|
||||||
==============
|
==============
|
||||||
|
|
||||||
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
|
>>> from pyicloud import PyiCloudService
|
||||||
>>> api = PyiCloudService('jappleseed@apple.com', 'password')
|
>>> api = PyiCloudService('jappleseed@apple.com', 'password')
|
||||||
|
|
||||||
In the event that the username/password combination is invalid, a ``PyiCloudFailedLoginException`` exception is thrown.
|
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
|
Devices
|
||||||
=======
|
=======
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
import six
|
import six
|
||||||
import copy
|
|
||||||
import uuid
|
import uuid
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import pickle
|
|
||||||
import requests
|
import requests
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
@ -19,6 +17,7 @@ from pyicloud.services import (
|
||||||
ContactsService,
|
ContactsService,
|
||||||
RemindersService
|
RemindersService
|
||||||
)
|
)
|
||||||
|
from pyicloud.utils import get_password_from_keyring
|
||||||
|
|
||||||
if six.PY3:
|
if six.PY3:
|
||||||
import http.cookiejar as cookielib
|
import http.cookiejar as cookielib
|
||||||
|
@ -39,7 +38,12 @@ class PyiCloudService(object):
|
||||||
pyicloud = PyiCloudService('username@apple.com', 'password')
|
pyicloud = PyiCloudService('username@apple.com', 'password')
|
||||||
pyicloud.iphone.location()
|
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.discovery = None
|
||||||
self.client_id = str(uuid.uuid1()).upper()
|
self.client_id = str(uuid.uuid1()).upper()
|
||||||
self.user = {'apple_id': apple_id, 'password': password}
|
self.user = {'apple_id': apple_id, 'password': password}
|
||||||
|
|
|
@ -9,7 +9,10 @@ import argparse
|
||||||
import pickle
|
import pickle
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from click import confirm
|
||||||
|
|
||||||
import pyicloud
|
import pyicloud
|
||||||
|
from . import utils
|
||||||
|
|
||||||
|
|
||||||
DEVICE_ERROR = (
|
DEVICE_ERROR = (
|
||||||
|
@ -52,7 +55,25 @@ def main(args=None):
|
||||||
action="store",
|
action="store",
|
||||||
dest="password",
|
dest="password",
|
||||||
default="",
|
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(
|
parser.add_argument(
|
||||||
"--list",
|
"--list",
|
||||||
|
@ -152,17 +173,57 @@ def main(args=None):
|
||||||
)
|
)
|
||||||
|
|
||||||
command_line = parser.parse_args(args)
|
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
|
username = command_line.username
|
||||||
try:
|
password = command_line.password
|
||||||
api = PyiCloudService(
|
|
||||||
command_line.username.strip(),
|
if username and command_line.delete_from_keyring:
|
||||||
command_line.password.strip()
|
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:
|
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:
|
for dev in api.devices:
|
||||||
if (
|
if (
|
||||||
|
|
|
@ -1,8 +1,15 @@
|
||||||
|
|
||||||
|
class PyiCloudException(Exception):
|
||||||
class PyiCloudNoDevicesException(Exception):
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class PyiCloudFailedLoginException(Exception):
|
class PyiCloudNoDevicesException(PyiCloudException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class PyiCloudFailedLoginException(PyiCloudException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NoStoredPasswordAvailable(PyiCloudException):
|
||||||
pass
|
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
|
requests>=1.2
|
||||||
|
keyring>=8.0,<9.0
|
||||||
|
keyrings.alt>=1.0,<2.0
|
||||||
|
click>=6.0,<7.0
|
||||||
six
|
six
|
||||||
pytz
|
pytz
|
||||||
certifi
|
certifi
|
||||||
|
|
Loading…
Reference in a new issue