lastpass 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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