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.
- 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
|