Skip to content

Commit

Permalink
Merge pull request #6 from konomae/py3
Browse files Browse the repository at this point in the history
Python 3 support

Close #6 and #5
  • Loading branch information
konomae committed May 22, 2014
2 parents 1037f85 + e2fbd0a commit 1bf6d13
Show file tree
Hide file tree
Showing 12 changed files with 242 additions and 223 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
language: python
python:
- "2.7"
- "3.3"
install:
- "pip install -r requirements.txt"
script:
Expand Down
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Example

vault = lastpass.Vault.open_remote(username, password)
for i in vault.accounts:
print i.id, i.username, i.password, i.url
print(i.id, i.username, i.password, i.url)


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


for index, i in enumerate(vault.accounts):
print index+1, i.id, i.name, i.username, i.password, i.url, i.group
print("{} {} {} {} {} {} {}".format(index + 1, i.id, i.name, i.username, i.password, i.url, i.group))
5 changes: 2 additions & 3 deletions lastpass/blob.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
# coding: utf-8
import fetcher

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 fetcher.Fetcher.make_key(username, password, self.key_iteration_count)
from .fetcher import Fetcher
return 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
38 changes: 21 additions & 17 deletions lastpass/fetcher.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
# coding: utf-8
import httplib
import pbkdf2
import hashlib
from base64 import b64decode
from binascii import hexlify
from Crypto.Hash import HMAC, SHA256
from Crypto.Protocol.KDF import PBKDF2
import requests
#from lxml import etree
from xml.etree import ElementTree as etree
import blob
from exceptions import (
from . import blob
from .exceptions import (
NetworkError,
InvalidResponseError,
UnknownResponseSchemaError,
Expand All @@ -16,7 +18,7 @@
LastPassIncorrectYubikeyPasswordError,
LastPassUnknownError
)
from session import Session
from .session import Session


class Fetcher(object):
Expand All @@ -30,7 +32,7 @@ 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})

if response.status_code != httplib.OK:
if response.status_code != requests.codes.ok:
raise NetworkError()

return blob.Blob(cls.decode_blob(response.content), session.key_iteration_count)
Expand All @@ -39,7 +41,7 @@ def fetch(cls, session, web_client=requests):
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:
if response.status_code != requests.codes.ok:
raise NetworkError()

try:
Expand Down Expand Up @@ -68,7 +70,7 @@ def request_login(cls, username, password, key_iteration_count, multifactor_pass
response = web_client.post('https://lastpass.com/login.php',
data=body)

if response.status_code != httplib.OK:
if response.status_code != requests.codes.ok:
raise NetworkError()

try:
Expand All @@ -88,7 +90,7 @@ def request_login(cls, username, password, key_iteration_count, multifactor_pass
def create_session(cls, parsed_response, key_iteration_count):
if parsed_response.tag == 'ok':
session_id = parsed_response.attrib.get('sessionid')
if isinstance(session_id, basestring):
if isinstance(session_id, str):
return Session(session_id, key_iteration_count)

@classmethod
Expand All @@ -114,24 +116,26 @@ def login_error(cls, parsed_response):

@classmethod
def decode_blob(cls, blob):
return blob.decode('base64')
return b64decode(blob)

@classmethod
def make_key(cls, username, password, key_iteration_count):
if key_iteration_count == 1:
return hashlib.sha256(username + password).digest()
return hashlib.sha256(username.encode() + password.encode()).digest()
else:
return pbkdf2.pbkdf2_bin(password, username, key_iteration_count, 32, hashlib.sha256)
prf = lambda p, s: HMAC.new(p, s, SHA256).digest()
return PBKDF2(password.encode(), username.encode(), 32, key_iteration_count, prf)

@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()
return bytearray(hashlib.sha256(hexlify(cls.make_key(username, password, 1)) + password.encode()).hexdigest(), 'ascii')
else:
return pbkdf2.pbkdf2_hex(
prf = lambda p, s: HMAC.new(p, s, SHA256).digest()
return hexlify(PBKDF2(
cls.make_key(username, password, key_iteration_count),
password,
1,
password.encode(),
32,
hashlib.sha256)
1,
prf))

36 changes: 23 additions & 13 deletions lastpass/parser.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
# coding: utf-8
from StringIO import StringIO
from base64 import b64decode
import binascii
import codecs
from io import BytesIO
from collections import OrderedDict
from chunk import Chunk
import struct

from Crypto.Cipher import AES
from account import Account
from .account import Account
from .chunk import Chunk


class Parser(object):
# Splits the blob into chucks grouped by kind.
@classmethod
def extract_chunks(cls, blob):
chunks = OrderedDict()
stream = StringIO(blob.bytes)
while stream.pos < stream.len:
stream = BytesIO(blob.bytes)
current_pos = stream.tell()
stream.seek(0, 2)
length = stream.tell()
stream.seek(current_pos, 0)
while stream.tell() < length:
chunk = cls.read_chunk(stream)
if not chunks.get(chunk.id):
chunks[chunk.id] = []
Expand All @@ -26,7 +33,7 @@ def extract_chunks(cls, blob):
# TODO: See if this should be part of Account class.
@classmethod
def parse_account(cls, chunk, encryption_key):
io = StringIO(chunk.payload)
io = BytesIO(chunk.payload)
id = cls.read_item(io)
name = cls.decode_aes256_auto(cls.read_item(io), encryption_key)
group = cls.decode_aes256_auto(cls.read_item(io), encryption_key)
Expand Down Expand Up @@ -91,12 +98,15 @@ def read_uint32(cls, stream):
# Decodes a hex encoded string into raw bytes.
@classmethod
def decode_hex(cls, data):
return data.decode('hex')
try:
return codecs.decode(data, 'hex_codec')
except binascii.Error:
raise TypeError()

# Decodes a base64 encoded string into raw bytes.
@classmethod
def decode_base64(cls, data):
return data.decode('base64')
return b64decode(data)

# Guesses AES encoding/cipher from the length of the data.
# Possible combinations are:
Expand All @@ -109,7 +119,7 @@ def decode_aes256_auto(cls, data, encryption_key):
length64 = length % 64

if length == 0:
return ''
return b''
elif length16 == 0:
return cls.decode_aes256_ecb_plain(data, encryption_key)
elif length64 == 0 or length64 == 24 or length64 == 44:
Expand All @@ -125,7 +135,7 @@ def decode_aes256_auto(cls, data, encryption_key):
@classmethod
def decode_aes256_ecb_plain(cls, data, encryption_key):
if not data:
return ''
return b''
else:
return cls.decode_aes256('ecb', '', data, encryption_key)

Expand All @@ -138,7 +148,7 @@ def decode_aes256_ecb_base64(cls, data, encryption_key):
@classmethod
def decode_aes256_cbc_plain(cls, data, encryption_key):
if not data:
return ''
return b''
else:
# LastPass AES-256/CBC encryted string starts with an "!".
# Next 16 bytes are the IV for the cipher.
Expand All @@ -149,7 +159,7 @@ def decode_aes256_cbc_plain(cls, data, encryption_key):
@classmethod
def decode_aes256_cbc_base64(cls, data, encryption_key):
if not data:
return ''
return b''
else:
# LastPass AES-256/CBC/base64 encryted string starts with an "!".
# Next 24 bytes are the base64 encoded IV for the cipher.
Expand All @@ -175,5 +185,5 @@ def decode_aes256(cls, cipher, iv, data, encryption_key):
aes = AES.new(encryption_key, aes_mode, iv)
d = aes.decrypt(data)
# http://passingcuriosity.com/2009/aes-encryption-in-python-with-m2crypto/
unpad = lambda s: s[0:-ord(s[-1])]
unpad = lambda s: s[0:-ord(d[-1:])]
return unpad(d)
9 changes: 5 additions & 4 deletions lastpass/vault.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# coding: utf-8
import parser, fetcher
from .fetcher import Fetcher
from .parser import Parser


class Vault(object):
Expand All @@ -22,9 +23,9 @@ def open(cls, blob, username, password):
# Just fetches the blob, could be used to store it locally
@classmethod
def fetch_blob(cls, username, password, multifactor_password=None):
return fetcher.Fetcher.fetch(fetcher.Fetcher.login(username, password, multifactor_password))
return Fetcher.fetch(Fetcher.login(username, password, multifactor_password))

# This more of an internal method, use one of the static constructors instead
def __init__(self, blob, encryption_key):
chunks = parser.Parser.extract_chunks(blob)
self.accounts = [parser.Parser.parse_account(i, encryption_key) for i in chunks['ACCT']]
chunks = Parser.extract_chunks(blob)
self.accounts = [Parser.parse_account(i, encryption_key) for i in chunks[b'ACCT']]
3 changes: 1 addition & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
requests==1.1.0
simple-pbkdf2==1.0
requests>=1.2.1,<=3.0.0
pycrypto==2.6.1
mock==1.0.1
5 changes: 3 additions & 2 deletions tests/test_blob.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
# coding: utf-8
from base64 import b64decode
import unittest
from lastpass.blob import Blob


class BlobTestCase(unittest.TestCase):
def setUp(self):
self.bytes = 'TFBBVgAAAAMxMjJQUkVNAAAACjE0MTQ5'.decode('base64')
self.bytes = b64decode('TFBBVgAAAAMxMjJQUkVNAAAACjE0MTQ5')
self.key_iteration_count = 500
self.username = 'postlass@gmail.com'
self.password = 'pl1234567890'
self.encryption_key = 'OfOUvVnQzB4v49sNh4+PdwIFb9Fr5+jVfWRTf+E2Ghg='.decode('base64')
self.encryption_key = b64decode('OfOUvVnQzB4v49sNh4+PdwIFb9Fr5+jVfWRTf+E2Ghg=')

self.blob = Blob(self.bytes, self.key_iteration_count)

Expand Down

0 comments on commit 1bf6d13

Please sign in to comment.