Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ example/credentials.json

*.egg-info/
dist/
build
.tox
.eggs

.python-version
.idea/
Expand Down
2 changes: 1 addition & 1 deletion example/example.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,4 @@


for index, i in enumerate(vault.accounts):
print("{} {} {} {} {} {} {} {}".format(index + 1, i.id, i.name, i.username, i.password, i.url, i.group, i.notes))
print("{} {}".format(index + 1, str(i)))
41 changes: 39 additions & 2 deletions lastpass/account.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,47 @@
# coding: utf-8
import types


class Account(object):
def __init__(self, id, name, username, password, url, group, notes=None):
self.id = id
"""
Lastpass Password Account
"""
def __init__(self, _id, name, username, password, url, group, notes=None):
self.id = _id
self.name = name
self.username = username
self.password = password
self.url = url
self.group = group
self.notes = notes

def notes_string(self):
if type(self.notes) == bytes:
note_str = '{}'.format(self.notes.decode())
else:
note_str = '{}'.format(str(self.notes))
return note_str

def fields(self):
result_fields = []
for field in dir(self):
if not field.startswith('_') and not callable(getattr(self, field)):
result_fields.append(field)
return result_fields

def __str__(self):
return "name: {}\n\tusername: {}\n\tpassword: {}\n\turl: {}\n\tgroup: {}\n\tnotes: {}".format(self.name, self.username, self.password, self.url, self.group, self.notes_string())


class SecureNote(Account):
"""
Lastpass Secure Note
"""
def __init__(self):
pass

def __str__(self):
try:
return getattr(self, 'unparsed_notes_0').decode()
except AttributeError:
return '\n'.join(['\t\t{}: {}'.format(field, getattr(self, field).decode()) for field in self.fields()])
4 changes: 2 additions & 2 deletions lastpass/blob.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# coding: utf-8
class Blob(object):
def __init__(self, bytes, key_iteration_count):
self.bytes = bytes
def __init__(self, bytes_, key_iteration_count):
self.bytes = bytes_
self.key_iteration_count = key_iteration_count

def encryption_key(self, username, password):
Expand Down
4 changes: 2 additions & 2 deletions lastpass/chunk.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# coding: utf-8
class Chunk(object):
def __init__(self, id, payload):
self.id = id
def __init__(self, id_, payload):
self.id = id_
self.payload = payload

def __eq__(self, other):
Expand Down
10 changes: 10 additions & 0 deletions lastpass/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,16 @@ class LastPassIncorrectYubikeyPasswordError(Error):
pass


class LastPassIncorrectOutOfBandRequiredError(Error):
"""LastPass error: need to provide out of band authentication (e.g, LastPass Authenticator)"""
pass


class LastPassIncorrectMultiFactorResponseError(Error):
"""LastPass error: Multifactor response failed (wrong code or denied)"""
pass


class LastPassUnknownError(Error):
"""LastPass error we don't know about"""
pass
124 changes: 97 additions & 27 deletions lastpass/fetcher.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
# coding: utf-8
import hashlib
import random
import string
from base64 import b64decode
from binascii import hexlify
import requests
from xml.etree import ElementTree as etree
import requests

from . import blob
from .version import __version__
from .exceptions import (
Expand All @@ -14,6 +17,8 @@
LastPassInvalidPasswordError,
LastPassIncorrectGoogleAuthenticatorCodeError,
LastPassIncorrectYubikeyPasswordError,
LastPassIncorrectOutOfBandRequiredError,
LastPassIncorrectMultiFactorResponseError,
LastPassUnknownError
)
from .session import Session
Expand All @@ -23,9 +28,9 @@
headers = {'user-agent': 'lastpass-python/{}'.format(__version__)}


def login(username, password, multifactor_password=None, client_id=None):
def login(username, password, multifactor_password=None, client_id=None, trust_id=None, trust_me=False):
key_iteration_count = request_iteration_count(username)
return request_login(username, password, key_iteration_count, multifactor_password, client_id)
return request_login(username, password, key_iteration_count, multifactor_password, client_id, trust_id=trust_id, trust_me=trust_me)


def logout(session, web_client=http):
Expand Down Expand Up @@ -58,29 +63,36 @@ def request_iteration_count(username, web_client=http):

try:
count = int(response.content)
except:
except Exception:
raise InvalidResponseError('Key iteration count is invalid')

if count > 0:
return count
raise InvalidResponseError('Key iteration count is not positive')


def request_login(username, password, key_iteration_count, multifactor_password=None, client_id=None, web_client=http):
def request_login(username, password, key_iteration_count, multifactor_password=None, client_id=None, web_client=http, trust_id=None, trust_me=False):
body = {
'method': 'mobile',
'web': 1,
'xml': 1,
'method': 'cli',
'xml': 2,
'username': username,
'hash': make_hash(username, password, key_iteration_count),
'iterations': key_iteration_count,
'includeprivatekeyenc': 1,
'outofbandsupported': 1
}

if multifactor_password:
body['otp'] = multifactor_password

if trust_me and not trust_id:
trust_id = generate_trust_id()

if trust_id:
body['uuid'] = trust_id

if client_id:
body['imei'] = client_id
body['trustlabel'] = client_id

response = web_client.post('https://lastpass.com/login.php',
data=body,
Expand All @@ -97,22 +109,80 @@ def request_login(username, password, key_iteration_count, multifactor_password=
if parsed_response is None:
raise InvalidResponseError()

session = create_session(parsed_response, key_iteration_count)
session = create_session(parsed_response, key_iteration_count, trust_id)
if not session:
raise login_error(parsed_response)
try:
raise login_error(parsed_response)
except LastPassIncorrectOutOfBandRequiredError:
(session, parsed_response) = oob_login(web_client, parsed_response, body, key_iteration_count, trust_id)
if not session:
raise login_error(parsed_response)
if trust_me:
response = web_client.post('https://lastpass.com/trust.php', cookies={'PHPSESSID': session.id}, data={"token": session.token, "uuid": trust_id, "trustlabel": client_id})

return session


def create_session(parsed_response, key_iteration_count):
def oob_login(web_client, parsed_response, body, key_iteration_count, trust_id):
error = None if parsed_response.tag != 'response' else parsed_response.find(
'error')
if 'outofbandname' not in error.attrib or 'capabilities' not in error.attrib:
return (None, parsed_response)
oob_capabilities = error.attrib['capabilities'].split(',')
can_do_passcode = 'passcode' in oob_capabilities
if can_do_passcode and 'outofband' not in oob_capabilities:
return (None, parsed_response)
body['outofbandrequest'] = '1'
retries = 0
# loop waiting for out of band approval, or failure
while retries < 5:
retries += 1
response = web_client.post("https://lastpass.com/login.php", data=body)
if response.status_code != requests.codes.ok:
raise NetworkError()

try:
parsed_response = etree.fromstring(response.content)
except etree.ParseError:
parsed_response = None

if parsed_response is None:
raise InvalidResponseError()

session = create_session(parsed_response, key_iteration_count, trust_id)
if session:
return (session, parsed_response)
error = None if parsed_response.tag != 'response' else parsed_response.find(
'error')
if 'cause' in error.attrib and error.attrib['cause'] == 'outofbandrequired':
if 'retryid' in error.attrib:
body['outofbandretryid'] = error.attrib['retryid']
body['outofbandretry'] = "1"
continue
return (None, parsed_response)
return (None, parsed_response)


def generate_trust_id():
return ''.join(random.choice(string.ascii_uppercase + string.digits + string.ascii_lowercase + "!@#$") for _ in range(32))


def create_session(parsed_response, key_iteration_count, trust_id):
if parsed_response.tag == 'ok':
session_id = parsed_response.attrib.get('sessionid')
ok_response = parsed_response
else:
ok_response = parsed_response.find("ok")
if ok_response is not None:
session_id = ok_response.attrib.get('sessionid')
token = ok_response.attrib.get('token')
if isinstance(session_id, str):
return Session(session_id, key_iteration_count)
return Session(session_id, key_iteration_count, token, trust_id)
return None


def login_error(parsed_response):
error = None if parsed_response.tag != 'response' else parsed_response.find('error')
if error is None or len(error.attrib) == 0:
if error is None or not error.attrib:
raise UnknownResponseSchemaError()

exceptions = {
Expand All @@ -121,6 +191,8 @@ def login_error(parsed_response):
"googleauthrequired": LastPassIncorrectGoogleAuthenticatorCodeError,
"googleauthfailed": LastPassIncorrectGoogleAuthenticatorCodeError,
"yubikeyrestricted": LastPassIncorrectYubikeyPasswordError,
"outofbandrequired": LastPassIncorrectOutOfBandRequiredError,
"multifactorresponsefailed": LastPassIncorrectMultiFactorResponseError,
}

cause = error.attrib.get('cause')
Expand All @@ -131,27 +203,25 @@ def login_error(parsed_response):
return InvalidResponseError(message)


def decode_blob(blob):
return b64decode(blob)
def decode_blob(blob_):
return b64decode(blob_)


def make_key(username, password, key_iteration_count):
# type: (str, str, int) -> bytes
if key_iteration_count == 1:
return hashlib.sha256(username.encode('utf-8') + password.encode('utf-8')).digest()
else:
return hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), username.encode('utf-8'), key_iteration_count, 32)
return hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), username.encode('utf-8'), key_iteration_count, 32)


def make_hash(username, password, key_iteration_count):
# type: (str, str, int) -> bytes
if key_iteration_count == 1:
return bytearray(hashlib.sha256(hexlify(make_key(username, password, 1)) + password.encode('utf-8')).hexdigest(), 'ascii')
else:
return hexlify(hashlib.pbkdf2_hmac(
'sha256',
make_key(username, password, key_iteration_count),
password.encode('utf-8'),
1,
32
))
return hexlify(hashlib.pbkdf2_hmac(
'sha256',
make_key(username, password, key_iteration_count),
password.encode('utf-8'),
1,
32
))
Loading