lastpass 1.3.0 → 1.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +8 -0
- data/example/example.rb +5 -3
- data/lib/lastpass/fetcher.rb +11 -2
- data/lib/lastpass/http.rb +3 -3
- data/lib/lastpass/parser.rb +12 -12
- data/lib/lastpass/vault.rb +28 -7
- data/lib/lastpass/version.rb +1 -1
- data/spec/fetcher_spec.rb +30 -5
- data/spec/http_spec.rb +17 -9
- data/spec/parser_spec.rb +5 -4
- data/spec/vault_spec.rb +12 -0
- metadata +2 -2
data/CHANGELOG.md
CHANGED
@@ -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
|
|
data/example/example.rb
CHANGED
@@ -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|
|
data/lib/lastpass/fetcher.rb
CHANGED
@@ -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,
|
data/lib/lastpass/http.rb
CHANGED
data/lib/lastpass/parser.rb
CHANGED
@@ -48,14 +48,14 @@ module LastPass
|
|
48
48
|
|
49
49
|
# Parse secure note
|
50
50
|
if secure_note == "1"
|
51
|
-
|
52
|
-
|
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
|
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
|
-
|
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
|
-
|
132
|
+
info
|
133
133
|
end
|
134
134
|
|
135
135
|
# Reads one chunk from a stream and creates a Chunk object with the data read.
|
data/lib/lastpass/vault.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/lastpass/version.rb
CHANGED
data/spec/fetcher_spec.rb
CHANGED
@@ -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
|
data/spec/http_spec.rb
CHANGED
@@ -4,14 +4,22 @@
|
|
4
4
|
require "spec_helper"
|
5
5
|
|
6
6
|
describe LastPass::HTTP do
|
7
|
-
|
7
|
+
let(:http) { LastPass::HTTP }
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
data/spec/parser_spec.rb
CHANGED
@@ -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) { "
|
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
|
-
|
197
|
-
expect(result
|
198
|
-
expect(result
|
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
|
|
data/spec/vault_spec.rb
CHANGED
@@ -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.
|
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:
|
12
|
+
date: 2015-01-29 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: httparty
|