Skip to content

Commit

Permalink
Ported: 46e1c3b3e29dbad194d55f4052d9771f4fee9813
Browse files Browse the repository at this point in the history
  • Loading branch information
konomae committed Dec 29, 2013
1 parent c78bd7c commit 7832eb0
Show file tree
Hide file tree
Showing 26 changed files with 1,849 additions and 331 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
example/credentials.json
9 changes: 4 additions & 5 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,11 @@ Example
::

# coding: utf-8
from lastpass import Fetcher, Parser
import lastpass

fetcher = Fetcher.fetch('username', 'password')
parser = Parser.parse(fetcher.blob, fetcher.encryption_key)
accounts = parser.chunks['ACCT']
print accounts
vault = lastpass.Vault.open_remote(username, password)
for i in vault.accounts:
print i.id, i.username, i.password, i.url


License
Expand Down
7 changes: 0 additions & 7 deletions example.py

This file was deleted.

Empty file added example/__init__.py
Empty file.
4 changes: 4 additions & 0 deletions example/credentials.json.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"username": "account@example.com",
"password": "correct horse battery staple"
}
29 changes: 29 additions & 0 deletions example/example.py
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
5 changes: 4 additions & 1 deletion lastpass/__init__.py
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 *
9 changes: 9 additions & 0 deletions lastpass/account.py
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
14 changes: 14 additions & 0 deletions lastpass/blob.py
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
8 changes: 8 additions & 0 deletions lastpass/chunk.py
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
45 changes: 45 additions & 0 deletions lastpass/exceptions.py
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
165 changes: 110 additions & 55 deletions lastpass/fetcher.py
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
Loading

0 comments on commit 7832eb0

Please sign in to comment.