Skip to content

Commit

Permalink
Don't parse PRIK chunks, request the private key during login and sto…
Browse files Browse the repository at this point in the history
…re it in the session and the blob
  • Loading branch information
detunized committed Apr 15, 2018
1 parent 8138d9e commit c3b11d3
Show file tree
Hide file tree
Showing 10 changed files with 66 additions and 50 deletions.
6 changes: 4 additions & 2 deletions lib/lastpass/blob.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
module LastPass
class Blob
attr_reader :bytes,
:key_iteration_count
:key_iteration_count,
:encrypted_private_key

def initialize bytes, key_iteration_count
def initialize bytes, key_iteration_count, encrypted_private_key
@bytes = bytes
@key_iteration_count = key_iteration_count
@encrypted_private_key = encrypted_private_key
end

def encryption_key username, password
Expand Down
9 changes: 6 additions & 3 deletions lib/lastpass/fetcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ def self.fetch session, web_client = http

raise NetworkError unless response.response.is_a? Net::HTTPOK

Blob.new decode_blob(response.parsed_response), session.key_iteration_count
Blob.new decode_blob(response.parsed_response),
session.key_iteration_count,
session.encrypted_private_key
end

def self.request_iteration_count username, web_client = http
Expand Down Expand Up @@ -55,7 +57,8 @@ def self.request_login username,
xml: 1,
username: username,
hash: make_hash(username, password, key_iteration_count),
iterations: key_iteration_count
iterations: key_iteration_count,
includeprivatekeyenc: 1
}

body[:otp] = multifactor_password if multifactor_password
Expand All @@ -79,7 +82,7 @@ def self.create_session parsed_response, key_iteration_count
if ok.is_a? Hash
session_id = ok["sessionid"]
if session_id.is_a? String
return Session.new session_id, key_iteration_count
return Session.new session_id, key_iteration_count, ok["privatekeyenc"]
end
end

Expand Down
48 changes: 24 additions & 24 deletions lib/lastpass/parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,30 +62,6 @@ def self.parse_ACCT chunk, encryption_key
end
end

# Parse PRIK chunk which contains private RSA key
def self.parse_PRIK chunk, encryption_key
decrypted = decode_aes256 "cbc",
encryption_key[0, 16],
decode_hex(chunk.payload),
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
rsa_key.n = asn1_encoded_key.value[1].value
rsa_key.e = asn1_encoded_key.value[2].value
rsa_key.d = asn1_encoded_key.value[3].value
rsa_key.p = asn1_encoded_key.value[4].value
rsa_key.q = asn1_encoded_key.value[5].value
rsa_key.dmp1 = asn1_encoded_key.value[6].value
rsa_key.dmq1 = asn1_encoded_key.value[7].value
rsa_key.iqmp = asn1_encoded_key.value[8].value

rsa_key
end

# TODO: Fake some data and make a test
def self.parse_SHAR chunk, encryption_key, rsa_key
StringIO.open chunk.payload do |io|
Expand All @@ -112,6 +88,30 @@ def self.parse_SHAR chunk, encryption_key, rsa_key
end
end

# Parse and decrypt the encrypted private RSA key
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
rsa_key.n = asn1_encoded_key.value[1].value
rsa_key.e = asn1_encoded_key.value[2].value
rsa_key.d = asn1_encoded_key.value[3].value
rsa_key.p = asn1_encoded_key.value[4].value
rsa_key.q = asn1_encoded_key.value[5].value
rsa_key.dmp1 = asn1_encoded_key.value[6].value
rsa_key.dmq1 = asn1_encoded_key.value[7].value
rsa_key.iqmp = asn1_encoded_key.value[8].value

rsa_key
end

def self.parse_secure_note_server notes
info = {}

Expand Down
6 changes: 4 additions & 2 deletions lib/lastpass/session.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
module LastPass
class Session
attr_reader :id,
:key_iteration_count
:key_iteration_count,
:encrypted_private_key

def initialize id, key_iteration_count
def initialize id, key_iteration_count, encrypted_private_key
@id = id
@key_iteration_count = key_iteration_count
@encrypted_private_key = encrypted_private_key
end
end
end
17 changes: 10 additions & 7 deletions lib/lastpass/vault.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,21 @@ def initialize blob, encryption_key
raise InvalidResponseError, "Blob is truncated"
end

@accounts = parse_accounts chunks, encryption_key
private_key = nil
if blob.encrypted_private_key
private_key = Parser.parse_private_key blob.encrypted_private_key, encryption_key
end

@accounts = parse_accounts chunks, encryption_key, private_key
end

def complete? chunks
!chunks.empty? && chunks.last.id == "ENDM" && chunks.last.payload == "OK"
end

def parse_accounts chunks, encryption_key
def parse_accounts chunks, encryption_key, private_key
accounts = []

key = encryption_key
rsa_private_key = nil

chunks.each do |i|
case i.id
Expand All @@ -59,11 +62,11 @@ def parse_accounts chunks, encryption_key
if account
accounts << account
end
when "PRIK"
rsa_private_key = Parser.parse_PRIK i, encryption_key
when "SHAR"
raise "private_key must be provided" if !private_key

# After SHAR chunk all the folliwing accounts are enrypted with a new key
key = Parser.parse_SHAR(i, encryption_key, rsa_private_key)[:encryption_key]
key = Parser.parse_SHAR(i, encryption_key, private_key)[:encryption_key]
end
end

Expand Down
4 changes: 3 additions & 1 deletion spec/blob_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@
describe LastPass::Blob do
let(:bytes) { "TFBBVgAAAAMxMjJQUkVNAAAACjE0MTQ5".decode64 }
let(:key_iteration_count) { 500 }
let(:encrypted_private_key) { "DEADBEEF" }
let(:username) { "postlass@gmail.com" }
let(:password) { "pl1234567890" }
let(:encryption_key) { "OfOUvVnQzB4v49sNh4+PdwIFb9Fr5+jVfWRTf+E2Ghg=".decode64 }

subject { LastPass::Blob.new bytes, key_iteration_count }
subject { LastPass::Blob.new bytes, key_iteration_count, encrypted_private_key }

its(:bytes) { should eq bytes }
its(:key_iteration_count) { should eq key_iteration_count }
its(:encrypted_private_key) { should eq encrypted_private_key }

describe "#encryption_key" do
it "returns encryption key" do
Expand Down
9 changes: 5 additions & 4 deletions spec/fetcher_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,19 @@

let(:hash) { "7880a04588cfab954aa1a2da98fd9c0d2c6eba4c53e36a94510e6dbf30759256" }
let(:session_id) { "53ru,Hb713QnEVM5zWZ16jMvxS0" }
let(:session) { LastPass::Session.new session_id, key_iteration_count }
let(:session) { LastPass::Session.new session_id, key_iteration_count, "DEADBEEF" }

let(:blob_response) { "TFBBVgAAAAMxMjJQUkVNAAAACjE0MTQ5" }
let(:blob_bytes) { blob_response.decode64 }
let(:blob) { LastPass::Blob.new blob_bytes, key_iteration_count }
let(:blob) { LastPass::Blob.new blob_bytes, key_iteration_count, "DEADBEEF" }

let(:login_post_data) { {method: "mobile",
web: 1,
xml: 1,
username: username,
hash: hash,
iterations: key_iteration_count} }
iterations: key_iteration_count,
includeprivatekeyenc: 1} }

let(:device_id) { "492378378052455" }
let(:login_post_data_with_device_id) { login_post_data.merge({imei: device_id}) }
Expand Down Expand Up @@ -98,7 +99,7 @@ def verify_post_request multifactor_password, device_id, post_data
web_client = double("web_client")
expect(web_client).to receive(:post)
.with("https://lastpass.com/login.php", format: :xml, body: post_data)
.and_return(http_ok("ok" => {"sessionid" => session_id}))
.and_return(http_ok("ok" => {"sessionid" => session_id, "privatekeyenc" => "DEADBEEF"}))

LastPass::Fetcher.request_login username,
password,
Expand Down
9 changes: 5 additions & 4 deletions spec/parser_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

describe LastPass::Parser do
let(:key_iteration_count) { 5000 }
let(:blob) { LastPass::Blob.new TEST_BLOB, key_iteration_count }
let(:blob) { LastPass::Blob.new TEST_BLOB, key_iteration_count, "DEADBEEF" }
let(:padding) { "BEEFFACE"}
let(:encryption_key) { "OfOUvVnQzB4v49sNh4+PdwIFb9Fr5+jVfWRTf+E2Ghg=".decode64 }
let(:encoded_rsa_key) { "98F3F5518AE7C03EBBF195A616361619033509FB1FFA0408E883B7C5E80381F8" +
Expand Down Expand Up @@ -114,9 +114,10 @@
end
end

describe ".parse_PRIK" do
let(:chunk) { LastPass::Chunk.new "PRIK", encoded_rsa_key }
let(:rsa_key) { LastPass::Parser.parse_PRIK chunk, rsa_key_encryption_key }
describe ".parse_private_key" do
let(:rsa_key) {
LastPass::Parser.parse_private_key encoded_rsa_key, rsa_key_encryption_key
}

it "parses private key" do
expect(rsa_key).to be_instance_of OpenSSL::PKey::RSA
Expand Down
4 changes: 3 additions & 1 deletion spec/session_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
describe LastPass::Session do
let(:id) { "53ru,Hb713QnEVM5zWZ16jMvxS0" }
let(:key_iteration_count) { 5000 }
let(:encrypted_private_key) { "DEADBEEF" }

subject { LastPass::Session.new id, key_iteration_count }
subject { LastPass::Session.new id, key_iteration_count, encrypted_private_key }

its(:id) { should eq id }
its(:key_iteration_count) { should eq key_iteration_count }
its(:encrypted_private_key) { should eq encrypted_private_key }
end
4 changes: 2 additions & 2 deletions spec/vault_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

describe LastPass::Vault do
let(:vault) {
LastPass::Vault.new LastPass::Blob.new(TEST_BLOB, TEST_KEY_ITERATION_COUNT),
LastPass::Vault.new LastPass::Blob.new(TEST_BLOB, TEST_KEY_ITERATION_COUNT, nil),
TEST_ENCRYPTION_KEY
}

Expand All @@ -15,7 +15,7 @@
[1, 2, 3, 4, 5, 10, 100, 1000].each do |i|
expect {
blob = TEST_BLOB[0..(-1 - i)]
LastPass::Vault.new LastPass::Blob.new(blob, TEST_KEY_ITERATION_COUNT),
LastPass::Vault.new LastPass::Blob.new(blob, TEST_KEY_ITERATION_COUNT, nil),
TEST_ENCRYPTION_KEY
}.to raise_error LastPass::InvalidResponseError, "Blob is truncated"
end
Expand Down

0 comments on commit c3b11d3

Please sign in to comment.