lastpass 1.3.0 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,11 @@
1
+ Version 1.4.0
2
+ -------------
3
+
4
+ - Added device id (IMEI/UUID) support
5
+ - Log out after fetching the blob to close the newly open session on LP server to prevent triggering anti-hacking logic (hopefully)
6
+ - Verify that the recieved blob is marked with ENDM chunk and hasn't been truncated in the process
7
+
8
+
1
9
  Version 1.3.0
2
10
  -------------
3
11
 
@@ -7,6 +7,8 @@
7
7
  require "lastpass"
8
8
  require "yaml"
9
9
 
10
+ DEVICE_ID = "example.rb"
11
+
10
12
  credentials = YAML.load_file File.join File.dirname(__FILE__), "credentials.yaml"
11
13
 
12
14
  username = credentials["username"]
@@ -14,21 +16,21 @@ password = credentials["password"]
14
16
 
15
17
  begin
16
18
  # First try without a multifactor password
17
- vault = LastPass::Vault.open_remote username, password
19
+ vault = LastPass::Vault.open_remote username, password, nil, DEVICE_ID
18
20
  rescue LastPass::LastPassIncorrectGoogleAuthenticatorCodeError => e
19
21
  # Get the code
20
22
  puts "Enter Google Authenticator code:"
21
23
  multifactor_password = gets.chomp
22
24
 
23
25
  # And now retry with the code
24
- vault = LastPass::Vault.open_remote username, password, multifactor_password
26
+ vault = LastPass::Vault.open_remote username, password, multifactor_password, DEVICE_ID
25
27
  rescue LastPass::LastPassIncorrectYubikeyPasswordError => e
26
28
  # Get the password
27
29
  puts "Enter Yubikey password:"
28
30
  multifactor_password = gets.chomp
29
31
 
30
32
  # And now retry with the Yubikey password
31
- vault = LastPass::Vault.open_remote username, password, multifactor_password
33
+ vault = LastPass::Vault.open_remote username, password, multifactor_password, DEVICE_ID
32
34
  end
33
35
 
34
36
  vault.accounts.each_with_index do |i, index|
@@ -3,9 +3,16 @@
3
3
 
4
4
  module LastPass
5
5
  class Fetcher
6
- def self.login username, password, multifactor_password = nil
6
+ def self.login username, password, multifactor_password = nil, client_id = nil
7
7
  key_iteration_count = request_iteration_count username
8
- request_login username, password, key_iteration_count, multifactor_password
8
+ request_login username, password, key_iteration_count, multifactor_password, client_id
9
+ end
10
+
11
+ def self.logout session, web_client = http
12
+ response = web_client.get "https://lastpass.com/logout.php?mobile=1",
13
+ cookies: {"PHPSESSID" => URI.encode(session.id)}
14
+
15
+ raise NetworkError unless response.response.is_a? Net::HTTPOK
9
16
  end
10
17
 
11
18
  def self.fetch session, web_client = http
@@ -39,6 +46,7 @@ module LastPass
39
46
  password,
40
47
  key_iteration_count,
41
48
  multifactor_password = nil,
49
+ client_id = nil,
42
50
  web_client = http
43
51
 
44
52
  body = {
@@ -51,6 +59,7 @@ module LastPass
51
59
  }
52
60
 
53
61
  body[:otp] = multifactor_password if multifactor_password
62
+ body[:imei] = client_id if client_id
54
63
 
55
64
  response = web_client.post "https://lastpass.com/login.php",
56
65
  format: :xml,
@@ -2,7 +2,7 @@
2
2
  # Licensed under the terms of the MIT license. See LICENCE for details.
3
3
 
4
4
  module LastPass
5
- class HTTP
6
- include HTTParty
7
- end
5
+ class HTTP
6
+ include HTTParty
7
+ end
8
8
  end
@@ -48,14 +48,14 @@ module LastPass
48
48
 
49
49
  # Parse secure note
50
50
  if secure_note == "1"
51
- 17.times { skip_item io }
52
- secure_note_type = read_item io
53
-
54
- if !ALLOWED_SECURE_NOTE_TYPES.key? secure_note_type
51
+ parsed = parse_secure_note_server notes
52
+ if !ALLOWED_SECURE_NOTE_TYPES.key? parsed[:type]
55
53
  return nil
56
54
  end
57
55
 
58
- url, username, password = parse_secure_note_server notes
56
+ url = parsed[:url] if parsed.key? :url
57
+ username = parsed[:username] if parsed.key? :username
58
+ password = parsed[:password] if parsed.key? :password
59
59
  end
60
60
 
61
61
  Account.new id, name, username, password, url, group
@@ -113,23 +113,23 @@ module LastPass
113
113
  end
114
114
 
115
115
  def self.parse_secure_note_server notes
116
- url = nil
117
- username = nil
118
- password = nil
116
+ info = {}
119
117
 
120
118
  notes.split("\n").each do |i|
121
119
  key, value = i.split ":", 2
122
120
  case key
121
+ when "NoteType"
122
+ info[:type] = value
123
123
  when "Hostname"
124
- url = value
124
+ info[:url] = value
125
125
  when "Username"
126
- username = value
126
+ info[:username] = value
127
127
  when "Password"
128
- password = value
128
+ info[:password] = value
129
129
  end
130
130
  end
131
131
 
132
- [url, username, password]
132
+ info
133
133
  end
134
134
 
135
135
  # Reads one chunk from a stream and creates a Chunk object with the data read.
@@ -6,13 +6,15 @@ module LastPass
6
6
  attr_reader :accounts
7
7
 
8
8
  # Fetches a blob from the server and creates a vault
9
- def self.open_remote username, password, multifactor_password = nil
10
- open Vault.fetch_blob(username, password, multifactor_password), username, password
9
+ def self.open_remote username, password, multifactor_password = nil, client_id = nil
10
+ blob = Vault.fetch_blob username, password, multifactor_password, client_id
11
+ open blob, username, password
11
12
  end
12
13
 
13
14
  # Creates a vault from a locally stored blob
14
15
  def self.open_local blob_filename, username, password
15
16
  # TODO: read the blob here
17
+ raise NotImplementedError
16
18
  end
17
19
 
18
20
  # Creates a vault from a blob object
@@ -21,24 +23,41 @@ module LastPass
21
23
  end
22
24
 
23
25
  # Just fetches the blob, could be used to store it locally
24
- def self.fetch_blob username, password, multifactor_password = nil
25
- Fetcher.fetch Fetcher.login username, password, multifactor_password
26
+ def self.fetch_blob username, password, multifactor_password = nil, client_id = nil
27
+ session = Fetcher.login username, password, multifactor_password, client_id
28
+ blob = Fetcher.fetch session
29
+ Fetcher.logout session
30
+
31
+ blob
26
32
  end
27
33
 
28
34
  # This more of an internal method, use one of the static constructors instead
29
35
  def initialize blob, encryption_key
30
- @accounts = []
36
+ chunks = Parser.extract_chunks blob
37
+ if !complete? chunks
38
+ raise InvalidResponseError, "Blob is truncated"
39
+ end
40
+
41
+ @accounts = parse_accounts chunks, encryption_key
42
+ end
43
+
44
+ def complete? chunks
45
+ !chunks.empty? && chunks.last.id == "ENDM" && chunks.last.payload == "OK"
46
+ end
47
+
48
+ def parse_accounts chunks, encryption_key
49
+ accounts = []
31
50
 
32
51
  key = encryption_key
33
52
  rsa_private_key = nil
34
53
 
35
- Parser.extract_chunks(blob).each do |i|
54
+ chunks.each do |i|
36
55
  case i.id
37
56
  when "ACCT"
38
57
  # TODO: Put shared folder name as group in the account
39
58
  account = Parser.parse_ACCT i, key
40
59
  if account
41
- @accounts << account
60
+ accounts << account
42
61
  end
43
62
  when "PRIK"
44
63
  rsa_private_key = Parser.parse_PRIK i, encryption_key
@@ -47,6 +66,8 @@ module LastPass
47
66
  key = Parser.parse_SHAR(i, encryption_key, rsa_private_key)[:encryption_key]
48
67
  end
49
68
  end
69
+
70
+ accounts
50
71
  end
51
72
  end
52
73
  end
@@ -2,5 +2,5 @@
2
2
  # Licensed under the terms of the MIT license. See LICENCE for details.
3
3
 
4
4
  module LastPass
5
- VERSION = "1.3.0"
5
+ VERSION = "1.4.0"
6
6
  end
@@ -23,12 +23,31 @@ describe LastPass::Fetcher do
23
23
  hash: hash,
24
24
  iterations: key_iteration_count} }
25
25
 
26
+ let(:device_id) { "492378378052455" }
27
+ let(:login_post_data_with_device_id) { login_post_data.merge({imei: device_id}) }
28
+
26
29
  let(:google_authenticator_code) { "123456" }
27
30
  let(:yubikey_password) { "emdbwzemyisymdnevznyqhqnklaqheaxszzvtnxjrmkb" }
28
31
 
29
- let(:login_post_data_with_google_authenticator_code) { login_post_data.merge({otp: google_authenticator_code})}
32
+ let(:login_post_data_with_google_authenticator_code) { login_post_data.merge({otp: google_authenticator_code}) }
30
33
  let(:login_post_data_with_yubikey_password) { login_post_data.merge({otp: yubikey_password}) }
31
34
 
35
+ describe ".logout" do
36
+ it "makes a GET request" do
37
+ web_client = double "web_client"
38
+ expect(web_client).to receive(:get)
39
+ .with("https://lastpass.com/logout.php?mobile=1", cookies: {"PHPSESSID" => session_id})
40
+ .and_return(http_ok "")
41
+ LastPass::Fetcher.logout session, web_client
42
+ end
43
+
44
+ it "raises an exception on HTTP error" do
45
+ expect {
46
+ LastPass::Fetcher.logout session, double("web_client", get: http_error)
47
+ }.to raise_error LastPass::NetworkError
48
+ end
49
+ end
50
+
32
51
  describe ".request_iteration_count" do
33
52
  it "makes a POST request" do
34
53
  expect(web_client = double("web_client")).to receive(:post)
@@ -75,7 +94,7 @@ describe LastPass::Fetcher do
75
94
  end
76
95
 
77
96
  describe ".request_login" do
78
- def verify_post_request multifactor_password, post_data
97
+ def verify_post_request multifactor_password, device_id, post_data
79
98
  web_client = double("web_client")
80
99
  expect(web_client).to receive(:post)
81
100
  .with("https://lastpass.com/login.php", format: :xml, body: post_data)
@@ -85,19 +104,24 @@ describe LastPass::Fetcher do
85
104
  password,
86
105
  key_iteration_count,
87
106
  multifactor_password,
107
+ device_id,
88
108
  web_client
89
109
  end
90
110
 
91
111
  it "makes a POST request" do
92
- verify_post_request nil, login_post_data
112
+ verify_post_request nil, nil, login_post_data
113
+ end
114
+
115
+ it "makes a POST request with device id" do
116
+ verify_post_request nil, device_id, login_post_data_with_device_id
93
117
  end
94
118
 
95
119
  it "makes a POST request with Google Authenticator code" do
96
- verify_post_request google_authenticator_code, login_post_data_with_google_authenticator_code
120
+ verify_post_request google_authenticator_code, nil, login_post_data_with_google_authenticator_code
97
121
  end
98
122
 
99
123
  it "makes a POST request with Yubikey password" do
100
- verify_post_request yubikey_password, login_post_data_with_yubikey_password
124
+ verify_post_request yubikey_password, nil, login_post_data_with_yubikey_password
101
125
  end
102
126
 
103
127
  it "returns a session" do
@@ -277,6 +301,7 @@ describe LastPass::Fetcher do
277
301
  password,
278
302
  key_iteration_count,
279
303
  nil,
304
+ nil,
280
305
  double("web_client", post: response)
281
306
  end
282
307
  end
@@ -4,14 +4,22 @@
4
4
  require "spec_helper"
5
5
 
6
6
  describe LastPass::HTTP do
7
- let(:http) { LastPass::HTTP }
7
+ let(:http) { LastPass::HTTP }
8
8
 
9
- it 'can set the proxy options' do
10
- http.http_proxy('proxy.fazbearentertainment.com', 1987, 'ffazbear', 'itsme')
11
- options = http.instance_variable_get(:@default_options)
12
- expect(options[:http_proxyaddr]).to eq('proxy.fazbearentertainment.com')
13
- expect(options[:http_proxyport]).to eq(1987)
14
- expect(options[:http_proxyuser]).to eq('ffazbear')
15
- expect(options[:http_proxypass]).to eq('itsme')
16
- end
9
+ describe "#http_proxy" do
10
+ let(:url) { "https://proxy.example.com" }
11
+ let(:port) { 12345 }
12
+ let(:username) { "username" }
13
+ let(:password) { "password" }
14
+
15
+ it "sets the proxy options" do
16
+ http.http_proxy url, port, username, password
17
+
18
+ options = http.instance_variable_get :@default_options
19
+ expect(options[:http_proxyaddr]).to eq url
20
+ expect(options[:http_proxyport]).to eq port
21
+ expect(options[:http_proxyuser]).to eq username
22
+ expect(options[:http_proxypass]).to eq password
23
+ end
24
+ end
17
25
  end
@@ -186,16 +186,17 @@ describe LastPass::Parser do
186
186
  end
187
187
 
188
188
  describe ".parse_secure_note_server" do
189
+ let(:type) { "type"}
189
190
  let(:url) { "url" }
190
191
  let(:username) { "username" }
191
192
  let(:password) { "password" }
192
- let(:notes) { "Hostname:#{url}\nUsername:#{username}\nPassword:#{password}" }
193
+ let(:notes) { "NoteType:#{type}\nHostname:#{url}\nUsername:#{username}\nPassword:#{password}" }
193
194
 
194
195
  it "returns parsed values" do
195
196
  result = LastPass::Parser.parse_secure_note_server notes
196
- expect(result[0]).to eq url
197
- expect(result[1]).to eq username
198
- expect(result[2]).to eq password
197
+
198
+ expect(result).to be_instance_of Hash
199
+ expect(result).to eq(type: type, url: url, username: username, password: password)
199
200
  end
200
201
  end
201
202
 
@@ -10,6 +10,18 @@ describe LastPass::Vault do
10
10
  TEST_ENCRYPTION_KEY
11
11
  }
12
12
 
13
+ describe ".new" do
14
+ it "raises an exception on trucated blob" do
15
+ [1, 2, 3, 4, 5, 10, 100, 1000].each do |i|
16
+ expect {
17
+ blob = TEST_BLOB[0..(-1 - i)]
18
+ LastPass::Vault.new LastPass::Blob.new(blob, TEST_KEY_ITERATION_COUNT),
19
+ TEST_ENCRYPTION_KEY
20
+ }.to raise_error LastPass::InvalidResponseError, "Blob is truncated"
21
+ end
22
+ end
23
+ end
24
+
13
25
  describe "#accounts" do
14
26
  context "returned accounts" do
15
27
  it { expect(vault.accounts).to be_instance_of Array }
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lastpass
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 1.4.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2014-12-03 00:00:00.000000000 Z
12
+ date: 2015-01-29 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: httparty