-
Notifications
You must be signed in to change notification settings - Fork 62
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Ported: 46e1c3b3e29dbad194d55f4052d9771f4fee9813
- Loading branch information
Showing
26 changed files
with
1,849 additions
and
331 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
example/credentials.json |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
{ | ||
"username": "account@example.com", | ||
"password": "correct horse battery staple" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
# coding: utf-8 | ||
import json | ||
import os | ||
import lastpass | ||
|
||
with open(os.path.join(os.path.dirname(__file__), 'credentials.json')) as f: | ||
credentials = json.load(f) | ||
username = str(credentials['username']) | ||
password = str(credentials['password']) | ||
|
||
try: | ||
# First try without a multifactor password | ||
vault = lastpass.Vault.open_remote(username, password) | ||
except lastpass.LastPassIncorrectGoogleAuthenticatorCodeError as e: | ||
# Get the code | ||
multifactor_password = input('Enter Google Authenticator code:') | ||
|
||
# And now retry with the code | ||
vault = lastpass.Vault.open_remote(username, password, multifactor_password) | ||
except lastpass.LastPassIncorrectYubikeyPasswordError as e: | ||
# Get the code | ||
multifactor_password = input('Enter Yubikey password:') | ||
|
||
# And now retry with the code | ||
vault = lastpass.Vault.open_remote(username, password, multifactor_password) | ||
|
||
|
||
for index, i in enumerate(vault.accounts): | ||
print index+1, i.id, i.name, i.username, i.password, i.url, i.group |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,5 @@ | ||
from .fetcher import Fetcher | ||
from .parser import Parser | ||
from .parser import Parser | ||
|
||
from .vault import Vault | ||
from .exceptions import * |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
# coding: utf-8 | ||
class Account(object): | ||
def __init__(self, id, name, username, password, url, group): | ||
self.id = id | ||
self.name = name | ||
self.username = username | ||
self.password = password | ||
self.url = url | ||
self.group = group |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
# coding: utf-8 | ||
import lastpass | ||
|
||
|
||
class Blob(object): | ||
def __init__(self, bytes, key_iteration_count): | ||
self.bytes = bytes | ||
self.key_iteration_count = key_iteration_count | ||
|
||
def encryption_key(self, username, password): | ||
return lastpass.Fetcher.make_key(username, password, self.key_iteration_count) | ||
|
||
def __eq__(self, other): | ||
return self.bytes == other.bytes and self.key_iteration_count == other.key_iteration_count |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
# coding: utf-8 | ||
class Chunk(object): | ||
def __init__(self, id, payload): | ||
self.id = id | ||
self.payload = payload | ||
|
||
def __eq__(self, other): | ||
return self.id == other.id and self.payload == other.payload |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
# coding: utf-8 | ||
|
||
|
||
# Base class for all errors, should not be raised | ||
class Error(Exception): pass | ||
|
||
|
||
# | ||
# Generic errors | ||
# | ||
|
||
# Something went wrong with the network | ||
class NetworkError(Error): pass | ||
|
||
|
||
# Server responded with something we don't understand | ||
class InvalidResponseError(Error): pass | ||
|
||
|
||
# Server responded with XML we don't understand | ||
class UnknownResponseSchemaError(Error): pass | ||
|
||
|
||
# | ||
# LastPass returned errors | ||
# | ||
|
||
# LastPass error: unknown username | ||
class LastPassUnknownUsernameError(Error): pass | ||
|
||
|
||
# LastPass error: invalid password | ||
class LastPassInvalidPasswordError(Error): pass | ||
|
||
|
||
# LastPass error: missing or incorrect Google Authenticator code | ||
class LastPassIncorrectGoogleAuthenticatorCodeError(Error): pass | ||
|
||
|
||
# LastPass error: missing or incorrect Yubikey password | ||
class LastPassIncorrectYubikeyPasswordError(Error): pass | ||
|
||
|
||
# LastPass error we don't know about | ||
class LastPassUnknownError(Error): pass |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,82 +1,137 @@ | ||
# coding: utf-8 | ||
import httplib | ||
import pbkdf2 | ||
import hashlib | ||
import requests | ||
#from lxml import etree | ||
from xml.etree import ElementTree as etree | ||
from lastpass.blob import Blob | ||
from lastpass.exceptions import ( | ||
NetworkError, | ||
InvalidResponseError, | ||
UnknownResponseSchemaError, | ||
LastPassUnknownUsernameError, | ||
LastPassInvalidPasswordError, | ||
LastPassIncorrectGoogleAuthenticatorCodeError, | ||
LastPassIncorrectYubikeyPasswordError, | ||
LastPassUnknownError | ||
) | ||
from lastpass.session import Session | ||
|
||
|
||
class Fetcher(object): | ||
@classmethod | ||
def fetch(cls, username, password, iterations=1): | ||
fetcher = cls(username, password, iterations) | ||
fetcher._fetch() | ||
def login(cls, username, password, multifactor_password=None): | ||
key_iteration_count = cls.request_iteration_count(username) | ||
return cls.request_login(username, password, key_iteration_count, multifactor_password) | ||
|
||
return fetcher | ||
@classmethod | ||
def fetch(cls, session, web_client=requests): | ||
response = web_client.get('https://lastpass.com/getaccts.php?mobile=1&b64=1&hash=0.0', | ||
cookies={'PHPSESSID': session.id}) | ||
|
||
@staticmethod | ||
def make_key(username, password, iterations=1): | ||
if iterations == 1: | ||
return hashlib.sha256(username + password).digest() | ||
else: | ||
return pbkdf2.pbkdf2_bin(password, username, iterations, 32, hashlib.sha256) | ||
if response.status_code != httplib.OK: | ||
raise NetworkError() | ||
|
||
return Blob(cls.decode_blob(response.content), session.key_iteration_count) | ||
|
||
@classmethod | ||
def make_hash(cls, username, password, iterations=1): | ||
if iterations == 1: | ||
return hashlib.sha256(cls.make_key(username, password, 1).encode('hex') + password).hexdigest() | ||
else: | ||
return pbkdf2.pbkdf2_hex( | ||
cls.make_key(username, password, iterations), | ||
password, | ||
1, | ||
32, | ||
hashlib.sha256) | ||
def request_iteration_count(cls, username, web_client=requests): | ||
response = web_client.post('https://lastpass.com/iterations.php', | ||
data={'email': username}) | ||
if response.status_code != httplib.OK: | ||
raise NetworkError() | ||
|
||
def __init__(self, username, password, iterations): | ||
self.username = username | ||
self.password = password | ||
self.iterations = iterations | ||
try: | ||
count = int(response.content) | ||
except: | ||
raise InvalidResponseError('Key iteration count is invalid') | ||
|
||
def _fetch(self): | ||
self.blob = self._fetch_blob(self._login()) | ||
if count > 0: | ||
return count | ||
raise InvalidResponseError('Key iteration count is not positive') | ||
|
||
def _login(self): | ||
self.encryption_key = Fetcher.make_key(self.username, self.password, self.iterations) | ||
options = { | ||
@classmethod | ||
def request_login(cls, username, password, key_iteration_count, multifactor_password=None, web_client=requests): | ||
body = { | ||
'method': 'mobile', | ||
'web': 1, | ||
'xml': 1, | ||
'username': self.username, | ||
'hash': self.make_hash(self.username, self.password, self.iterations), | ||
'iterations': self.iterations, | ||
'username': username, | ||
'hash': cls.make_hash(username, password, key_iteration_count), | ||
'iterations': key_iteration_count, | ||
} | ||
|
||
url = 'https://lastpass.com/login.php' | ||
return self._handle_login_response(requests.post(url, data=options)) | ||
if multifactor_password: | ||
body['otp'] = multifactor_password | ||
|
||
response = web_client.post('https://lastpass.com/login.php', | ||
data=body) | ||
|
||
if response.status_code != httplib.OK: | ||
raise NetworkError() | ||
|
||
def _handle_login_response(self, response): | ||
if response.status_code != requests.codes['OK']: | ||
raise RuntimeError('Failed to login: "{}"'.format(response)) | ||
parsed_response = etree.fromstring(response.content) | ||
try: | ||
parsed_response = etree.fromstring(response.content) | ||
except etree.ParseError: | ||
parsed_response = None | ||
|
||
if parsed_response is None: | ||
raise InvalidResponseError() | ||
|
||
session = cls.create_session(parsed_response, key_iteration_count) | ||
if not session: | ||
raise cls.login_error(parsed_response) | ||
return session | ||
|
||
@classmethod | ||
def create_session(cls, parsed_response, key_iteration_count): | ||
if parsed_response.tag == 'ok': | ||
return parsed_response.attrib['sessionid'] | ||
|
||
if parsed_response.tag == 'response': | ||
error = parsed_response.find('error') | ||
if error.attrib.get('iterations'): | ||
self.iterations = int(error.attrib['iterations']) | ||
return self._login() | ||
elif error.attrib.get('message'): | ||
raise RuntimeError('Failed to login, LastPass says "{}"'.format(error.attrib['message'])) | ||
else: | ||
raise RuntimeError('Failed to login, LastPass responded with an unknown error') | ||
session_id = parsed_response.attrib.get('sessionid') | ||
if isinstance(session_id, basestring): | ||
return Session(session_id, key_iteration_count) | ||
|
||
@classmethod | ||
def login_error(cls, parsed_response): | ||
error = None if parsed_response.tag != 'response' else parsed_response.find('error') | ||
if error is None or len(error.attrib) == 0: | ||
raise UnknownResponseSchemaError() | ||
|
||
exceptions = { | ||
"unknownemail": LastPassUnknownUsernameError, | ||
"unknownpassword": LastPassInvalidPasswordError, | ||
"googleauthrequired": LastPassIncorrectGoogleAuthenticatorCodeError, | ||
"googleauthfailed": LastPassIncorrectGoogleAuthenticatorCodeError, | ||
"yubikeyrestricted": LastPassIncorrectYubikeyPasswordError, | ||
} | ||
|
||
cause = error.attrib.get('cause') | ||
message = error.attrib.get('message') | ||
|
||
if cause: | ||
return exceptions.get(cause, LastPassUnknownError)(message or cause) | ||
return InvalidResponseError(message) | ||
|
||
@classmethod | ||
def decode_blob(cls, blob): | ||
return blob.decode('base64') | ||
|
||
@classmethod | ||
def make_key(cls, username, password, key_iteration_count): | ||
if key_iteration_count == 1: | ||
return hashlib.sha256(username + password).digest() | ||
else: | ||
raise RuntimeError('Failed to login, the reason is unknown') | ||
return pbkdf2.pbkdf2_bin(password, username, key_iteration_count, 32, hashlib.sha256) | ||
|
||
def _fetch_blob(self, session_id): | ||
url = 'https://lastpass.com/getaccts.php?mobile=1&b64=1&hash=0.0' | ||
response = requests.get(url, cookies={'PHPSESSID': session_id}) | ||
@classmethod | ||
def make_hash(cls, username, password, key_iteration_count): | ||
if key_iteration_count == 1: | ||
return hashlib.sha256(cls.make_key(username, password, 1).encode('hex') + password).hexdigest() | ||
else: | ||
return pbkdf2.pbkdf2_hex( | ||
cls.make_key(username, password, key_iteration_count), | ||
password, | ||
1, | ||
32, | ||
hashlib.sha256) | ||
|
||
response.raise_for_status() | ||
return response.content |
Oops, something went wrong.