class LastPass::Parser

Constants

ACCOUNT_LIKE_SECURE_NOTE_TYPES

Secure note types that contain account-like information

RSA_PKCS1_OAEP_PADDING

OpenSSL constant

Public Class Methods

decode_aes256(cipher, iv, data, encryption_key) click to toggle source

Decrypt AES-256 bytes. Allowed ciphers are: :ecb, :cbc. If for :ecb iv is not used and should be set to “”.

# File lib/lastpass/parser.rb, line 292
def self.decode_aes256 cipher, iv, data, encryption_key
    aes = OpenSSL::Cipher.new "aes-256-#{cipher}"
    aes.decrypt
    aes.key = encryption_key
    aes.iv = iv
    aes.update(data) + aes.final
end
decode_aes256_base64_auto(data, encryption_key) click to toggle source

Guesses AES cipher (EBC or CBD) from the length of the base64 encoded data.

# File lib/lastpass/parser.rb, line 228
def self.decode_aes256_base64_auto data, encryption_key
    length = data.length

    if length == 0
        ""
    elsif data[0] == "!"
        decode_aes256_cbc_base64 data, encryption_key
    else
        decode_aes256_ecb_base64 data, encryption_key
    end
end
decode_aes256_cbc_base64(data, encryption_key) click to toggle source

Decrypts base64 encoded AES-256 CBC bytes.

# File lib/lastpass/parser.rb, line 272
def self.decode_aes256_cbc_base64 data, encryption_key
    if data.empty?
        ""
    else
        # LastPass AES-256/CBC/base64 encryted string starts with an "!".
        # Next 24 bytes are the base64 encoded IV for the cipher.
        # Then comes the "|".
        # And the rest is the base64 encoded encrypted payload.

        # TODO: Check for input validity!
        decode_aes256 :cbc,
                      decode_base64(data[1, 24]),
                      decode_base64(data[26..-1]),
                      encryption_key
    end
end
decode_aes256_cbc_plain(data, encryption_key) click to toggle source

Decrypts AES-256 CBC bytes.

# File lib/lastpass/parser.rb, line 255
def self.decode_aes256_cbc_plain data, encryption_key
    if data.empty?
        ""
    else
        # LastPass AES-256/CBC encryted string starts with an "!".
        # Next 16 bytes are the IV for the cipher.
        # And the rest is the encrypted payload.

        # TODO: Check for input validity!
        decode_aes256 :cbc,
                      data[1, 16],
                      data[17..-1],
                      encryption_key
    end
end
decode_aes256_ecb_base64(data, encryption_key) click to toggle source

Decrypts base64 encoded AES-256 ECB bytes.

# File lib/lastpass/parser.rb, line 250
def self.decode_aes256_ecb_base64 data, encryption_key
    decode_aes256_ecb_plain decode_base64(data), encryption_key
end
decode_aes256_ecb_plain(data, encryption_key) click to toggle source

Decrypts AES-256 ECB bytes.

# File lib/lastpass/parser.rb, line 241
def self.decode_aes256_ecb_plain data, encryption_key
    if data.empty?
        ""
    else
        decode_aes256 :ecb, "", data, encryption_key
    end
end
decode_aes256_plain_auto(data, encryption_key) click to toggle source

Guesses AES cipher (EBC or CBD) from the length of the plain data.

# File lib/lastpass/parser.rb, line 215
def self.decode_aes256_plain_auto data, encryption_key
    length = data.length

    if length == 0
        ""
    elsif data[0] == "!" && length % 16 == 1 && length > 32
        decode_aes256_cbc_plain data, encryption_key
    else
        decode_aes256_ecb_plain data, encryption_key
    end
end
decode_base64(data) click to toggle source

Decodes a base64 encoded string into raw bytes.

# File lib/lastpass/parser.rb, line 209
def self.decode_base64 data
    # TODO: Check for input validity!
    Base64.decode64 data
end
decode_hex(data) click to toggle source

Decodes a hex encoded string into raw bytes.

# File lib/lastpass/parser.rb, line 201
def self.decode_hex data
    raise ArgumentError, "Input length must be multple of 2" unless data.size % 2 == 0
    raise ArgumentError, "Input contains invalid characters" unless data =~ /^[0-9a-f]*$/i

    data.scan(/../).map { |i| i.to_i 16 }.pack "c*"
end
extract_chunks(blob) click to toggle source

Splits the blob into chucks grouped by kind.

# File lib/lastpass/parser.rb, line 18
def self.extract_chunks blob
    chunks = []

    StringIO.open blob.bytes do |stream|
        while !stream.eof?
            chunks.push read_chunk stream
        end
    end

    chunks
end
parse_ACCT(chunk, encryption_key) click to toggle source

Parses an account chunk, decrypts and creates an Account object. Returns either an Account or a Note object, in case of a generic note that doesn't represent an account. All secure notes are ACCTs but not all of them store account information.

TODO: Make a test case that covers secure note account

# File lib/lastpass/parser.rb, line 36
def self.parse_ACCT chunk, encryption_key
    StringIO.open chunk.payload do |io|
        id = read_item io
        name = decode_aes256_plain_auto read_item(io), encryption_key
        group = decode_aes256_plain_auto read_item(io), encryption_key
        url = decode_hex read_item io
        notes = decode_aes256_plain_auto read_item(io), encryption_key
        2.times { skip_item io }
        username = decode_aes256_plain_auto read_item(io), encryption_key
        password = decode_aes256_plain_auto read_item(io), encryption_key
        2.times { skip_item io }
        secure_note = read_item io

        # Parse secure note
        if secure_note == "1"
            parsed = parse_secure_note_server notes
            if !ACCOUNT_LIKE_SECURE_NOTE_TYPES.key? parsed[:type]
                return Note.new id, name, notes, group
            end

            url = parsed[:url] if parsed.key? :url
            username = parsed[:username] if parsed.key? :username
            password = parsed[:password] if parsed.key? :password
        end

        Account.new id, name, username, password, url, notes, group
    end
end
parse_SHAR(chunk, encryption_key, rsa_key) click to toggle source

TODO: Fake some data and make a test

# File lib/lastpass/parser.rb, line 66
def self.parse_SHAR chunk, encryption_key, rsa_key
    StringIO.open chunk.payload do |io|
        id = read_item io
        encrypted_key = decode_hex read_item io
        encrypted_name = read_item io
        2.times { skip_item io }
        key = read_item io

        # Shared folder encryption key might come already in pre-decrypted form,
        # where it's only AES encrypted with the regular encryption key.
        # When the key is blank, then there's a RSA encrypted key, which has to
        # be decrypted first before use.
        key = if key.empty?
            decode_hex rsa_key.private_decrypt(encrypted_key, RSA_PKCS1_OAEP_PADDING)
        else
            decode_hex decode_aes256_plain_auto(key, encryption_key)
        end

        name = decode_aes256_base64_auto encrypted_name, key

        # TODO: Return an object, not a hash
        {id: id, name: name, encryption_key: key}
    end
end
parse_private_key(encrypted_private_key, encryption_key) click to toggle source

Parse and decrypt the encrypted private RSA key

# File lib/lastpass/parser.rb, line 92
def self.parse_private_key encrypted_private_key, encryption_key
    decrypted = decode_aes256 "cbc",
                              encryption_key[0, 16],
                              decode_hex(encrypted_private_key),
                              encryption_key

    /^LastPassPrivateKey<(?<hex_key>.*)>LastPassPrivateKey$/ =~ decrypted
    asn1_encoded_all = OpenSSL::ASN1.decode decode_hex hex_key
    asn1_encoded_key = OpenSSL::ASN1.decode asn1_encoded_all.value[2].value

    rsa_key = OpenSSL::PKey::RSA.new
    n = asn1_encoded_key.value[1].value
    e = asn1_encoded_key.value[2].value
    d = asn1_encoded_key.value[3].value
    p = asn1_encoded_key.value[4].value
    q = asn1_encoded_key.value[5].value
    dmp1 = asn1_encoded_key.value[6].value
    dmq1 = asn1_encoded_key.value[7].value
    iqmp = asn1_encoded_key.value[8].value

    if rsa_key.respond_to? :set_key
        rsa_key.set_key n, e, d
        rsa_key.set_factors p, q
        rsa_key.set_crt_params dmp1, dmq1, iqmp
    else
        rsa_key.n = n
        rsa_key.e = e
        rsa_key.d = d
        rsa_key.p = p
        rsa_key.q = q
        rsa_key.dmp1 = dmp1
        rsa_key.dmq1 = dmq1
        rsa_key.iqmp = iqmp
    end

    rsa_key
end
parse_secure_note_server(notes) click to toggle source
# File lib/lastpass/parser.rb, line 130
def self.parse_secure_note_server notes
    info = {}

    notes.split("\n").each do |i|
        key, value = i.split ":", 2
        case key
        when "NoteType"
            info[:type] = value
        when "Hostname"
            info[:url] = value
        when "Username"
            info[:username] = value
        when "Password"
            info[:password] = value
        end
    end

    info
end
read_chunk(stream) click to toggle source

Reads one chunk from a stream and creates a Chunk object with the data read.

# File lib/lastpass/parser.rb, line 151
def self.read_chunk stream
    # LastPass blob chunk is made up of 4-byte ID,
    # big endian 4-byte size and payload of that size.
    #
    # Example:
    #   0000: "IDID"
    #   0004: 4
    #   0008: 0xDE 0xAD 0xBE 0xEF
    #   000C: --- Next chunk ---
    Chunk.new read_id(stream), read_payload(stream, read_size(stream))
end
read_id(stream) click to toggle source

Reads a chunk ID from a stream.

# File lib/lastpass/parser.rb, line 181
def self.read_id stream
    stream.read 4
end
read_item(stream) click to toggle source

Reads an item from a stream and returns it as a string of bytes.

# File lib/lastpass/parser.rb, line 164
def self.read_item stream
    # An item in an itemized chunk is made up of the
    # big endian size and the payload of that size.
    #
    # Example:
    #   0000: 4
    #   0004: 0xDE 0xAD 0xBE 0xEF
    #   0008: --- Next item ---
    read_payload stream, read_size(stream)
end
read_payload(stream, size) click to toggle source

Reads a payload of a given size from a stream.

# File lib/lastpass/parser.rb, line 191
def self.read_payload stream, size
    stream.read size
end
read_size(stream) click to toggle source

Reads a chunk or an item ID.

# File lib/lastpass/parser.rb, line 186
def self.read_size stream
    read_uint32 stream
end
read_uint32(stream) click to toggle source

Reads an unsigned 32 bit integer from a stream.

# File lib/lastpass/parser.rb, line 196
def self.read_uint32 stream
    stream.read(4).unpack("N").first
end
skip_item(stream) click to toggle source

Skips an item in a stream.

# File lib/lastpass/parser.rb, line 176
def self.skip_item stream
    read_item stream
end