lastpass 1.5.0 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 63a049d06efc466ce9ff2861ba45dc8c0797c0bb
4
- data.tar.gz: 3d818fcc7316b20729917c5e40edac24a2e6b408
2
+ SHA256:
3
+ metadata.gz: 7051161f3612e54ae85ce0930e91973c84fd12d0395c3c4703d18c78151d49c6
4
+ data.tar.gz: b586d087e2697a0f4f63d3157677bd85b6583828ab124b13d43d4c5dd65a6a0a
5
5
  SHA512:
6
- metadata.gz: 2ec4ba7d245d8bc025b85f386ae69743db765c8a319436017586465cedfec115f03e454bc8495c5d2e4e14296e0c1b13eca3f5aa0e72851fbdc6bd75554e7548
7
- data.tar.gz: 4df5d76964fa78f8a1cfcdc14086043dbf2a927249425fe67d4acff71dd7ed0047a5c95bb52e0d29da8d4e207fd59abef54770bdf4bbfa021a24c8887aba2eb8
6
+ metadata.gz: b3fff4b432c45316ee806e854d4b95c8c0e7e77671bed0d65ce091a611217bfd90878fd6e995845f3551d9927f0ecc4dbd3fafd88236f82dd6d2f00fd9a4f996
7
+ data.tar.gz: 32ef67cac6f64a4478210ba82eedf21a34e4e7f3edc1b66601db97473c675b6b662b4d91d0a5e746df2c66f194eb6feb341918528260c3a70d901bf78e4d89a4
@@ -5,3 +5,6 @@ rvm:
5
5
  - 2.2.6
6
6
  - 2.3.3
7
7
  - 2.4.0
8
+ deploy:
9
+ provider: rubygems
10
+ api_key: "YOUR API KEY"
@@ -1,3 +1,10 @@
1
+ Version 1.6.0
2
+ -------------
3
+
4
+ - Changed the way the private keys are parsed
5
+ - OpenSSL 1.1.0 support
6
+ - Bug fixes
7
+
1
8
  Version 1.5.0
2
9
  -------------
3
10
 
@@ -4,11 +4,13 @@
4
4
  module LastPass
5
5
  class Blob
6
6
  attr_reader :bytes,
7
- :key_iteration_count
7
+ :key_iteration_count,
8
+ :encrypted_private_key
8
9
 
9
- def initialize bytes, key_iteration_count
10
+ def initialize bytes, key_iteration_count, encrypted_private_key
10
11
  @bytes = bytes
11
12
  @key_iteration_count = key_iteration_count
13
+ @encrypted_private_key = encrypted_private_key
12
14
  end
13
15
 
14
16
  def encryption_key username, password
@@ -9,20 +9,22 @@ module LastPass
9
9
  end
10
10
 
11
11
  def self.logout session, web_client = http
12
- response = web_client.get "https://lastpass.com/logout.php?mobile=1",
12
+ response = web_client.get "https://lastpass.com/logout.php?method=cli&noredirect=1",
13
13
  cookies: {"PHPSESSID" => URI.encode(session.id)}
14
14
 
15
15
  raise NetworkError unless response.response.is_a? Net::HTTPOK
16
16
  end
17
17
 
18
18
  def self.fetch session, web_client = http
19
- response = web_client.get "https://lastpass.com/getaccts.php?mobile=1&b64=1&hash=0.0&hasplugin=3.0.23&requestsrc=android",
19
+ response = web_client.get "https://lastpass.com/getaccts.php?mobile=1&b64=1&hash=0.0&hasplugin=3.0.23&requestsrc=cli",
20
20
  format: :plain,
21
21
  cookies: {"PHPSESSID" => URI.encode(session.id)}
22
22
 
23
23
  raise NetworkError unless response.response.is_a? Net::HTTPOK
24
24
 
25
- Blob.new decode_blob(response.parsed_response), session.key_iteration_count
25
+ Blob.new decode_blob(response.parsed_response),
26
+ session.key_iteration_count,
27
+ session.encrypted_private_key
26
28
  end
27
29
 
28
30
  def self.request_iteration_count username, web_client = http
@@ -50,12 +52,12 @@ module LastPass
50
52
  web_client = http
51
53
 
52
54
  body = {
53
- method: "mobile",
54
- web: 1,
55
- xml: 1,
55
+ method: "cli",
56
+ xml: 2,
56
57
  username: username,
57
58
  hash: make_hash(username, password, key_iteration_count),
58
- iterations: key_iteration_count
59
+ iterations: key_iteration_count,
60
+ includeprivatekeyenc: 1
59
61
  }
60
62
 
61
63
  body[:otp] = multifactor_password if multifactor_password
@@ -75,11 +77,14 @@ module LastPass
75
77
  end
76
78
 
77
79
  def self.create_session parsed_response, key_iteration_count
78
- ok = parsed_response["ok"]
80
+ ok = (parsed_response["response"] || {})["ok"]
79
81
  if ok.is_a? Hash
80
82
  session_id = ok["sessionid"]
81
83
  if session_id.is_a? String
82
- return Session.new session_id, key_iteration_count
84
+ private_key = ok["privatekeyenc"]
85
+ private_key = nil if private_key == ""
86
+
87
+ return Session.new session_id, key_iteration_count, private_key
83
88
  end
84
89
  end
85
90
 
@@ -95,7 +100,7 @@ module LastPass
95
100
  "unknownpassword" => LastPassInvalidPasswordError,
96
101
  "googleauthrequired" => LastPassIncorrectGoogleAuthenticatorCodeError,
97
102
  "googleauthfailed" => LastPassIncorrectGoogleAuthenticatorCodeError,
98
- "yubikeyrestricted" => LastPassIncorrectYubikeyPasswordError,
103
+ "otprequired" => LastPassIncorrectYubikeyPasswordError,
99
104
  }
100
105
 
101
106
  cause = error["cause"]
@@ -62,30 +62,6 @@ module LastPass
62
62
  end
63
63
  end
64
64
 
65
- # Parse PRIK chunk which contains private RSA key
66
- def self.parse_PRIK chunk, encryption_key
67
- decrypted = decode_aes256 "cbc",
68
- encryption_key[0, 16],
69
- decode_hex(chunk.payload),
70
- encryption_key
71
-
72
- /^LastPassPrivateKey<(?<hex_key>.*)>LastPassPrivateKey$/ =~ decrypted
73
- asn1_encoded_all = OpenSSL::ASN1.decode decode_hex hex_key
74
- asn1_encoded_key = OpenSSL::ASN1.decode asn1_encoded_all.value[2].value
75
-
76
- rsa_key = OpenSSL::PKey::RSA.new
77
- rsa_key.n = asn1_encoded_key.value[1].value
78
- rsa_key.e = asn1_encoded_key.value[2].value
79
- rsa_key.d = asn1_encoded_key.value[3].value
80
- rsa_key.p = asn1_encoded_key.value[4].value
81
- rsa_key.q = asn1_encoded_key.value[5].value
82
- rsa_key.dmp1 = asn1_encoded_key.value[6].value
83
- rsa_key.dmq1 = asn1_encoded_key.value[7].value
84
- rsa_key.iqmp = asn1_encoded_key.value[8].value
85
-
86
- rsa_key
87
- end
88
-
89
65
  # TODO: Fake some data and make a test
90
66
  def self.parse_SHAR chunk, encryption_key, rsa_key
91
67
  StringIO.open chunk.payload do |io|
@@ -112,6 +88,45 @@ module LastPass
112
88
  end
113
89
  end
114
90
 
91
+ # Parse and decrypt the encrypted private RSA key
92
+ def self.parse_private_key encrypted_private_key, encryption_key
93
+ decrypted = decode_aes256 "cbc",
94
+ encryption_key[0, 16],
95
+ decode_hex(encrypted_private_key),
96
+ encryption_key
97
+
98
+ /^LastPassPrivateKey<(?<hex_key>.*)>LastPassPrivateKey$/ =~ decrypted
99
+ asn1_encoded_all = OpenSSL::ASN1.decode decode_hex hex_key
100
+ asn1_encoded_key = OpenSSL::ASN1.decode asn1_encoded_all.value[2].value
101
+
102
+ rsa_key = OpenSSL::PKey::RSA.new
103
+ n = asn1_encoded_key.value[1].value
104
+ e = asn1_encoded_key.value[2].value
105
+ d = asn1_encoded_key.value[3].value
106
+ p = asn1_encoded_key.value[4].value
107
+ q = asn1_encoded_key.value[5].value
108
+ dmp1 = asn1_encoded_key.value[6].value
109
+ dmq1 = asn1_encoded_key.value[7].value
110
+ iqmp = asn1_encoded_key.value[8].value
111
+
112
+ if rsa_key.respond_to? :set_key
113
+ rsa_key.set_key n, e, d
114
+ rsa_key.set_factors p, q
115
+ rsa_key.set_crt_params dmp1, dmq1, iqmp
116
+ else
117
+ rsa_key.n = n
118
+ rsa_key.e = e
119
+ rsa_key.d = d
120
+ rsa_key.p = p
121
+ rsa_key.q = q
122
+ rsa_key.dmp1 = dmp1
123
+ rsa_key.dmq1 = dmq1
124
+ rsa_key.iqmp = iqmp
125
+ end
126
+
127
+ rsa_key
128
+ end
129
+
115
130
  def self.parse_secure_note_server notes
116
131
  info = {}
117
132
 
@@ -275,7 +290,7 @@ module LastPass
275
290
  # Allowed ciphers are: :ecb, :cbc.
276
291
  # If for :ecb iv is not used and should be set to "".
277
292
  def self.decode_aes256 cipher, iv, data, encryption_key
278
- aes = OpenSSL::Cipher::Cipher.new "aes-256-#{cipher}"
293
+ aes = OpenSSL::Cipher.new "aes-256-#{cipher}"
279
294
  aes.decrypt
280
295
  aes.key = encryption_key
281
296
  aes.iv = iv
@@ -4,11 +4,13 @@
4
4
  module LastPass
5
5
  class Session
6
6
  attr_reader :id,
7
- :key_iteration_count
7
+ :key_iteration_count,
8
+ :encrypted_private_key
8
9
 
9
- def initialize id, key_iteration_count
10
+ def initialize id, key_iteration_count, encrypted_private_key
10
11
  @id = id
11
12
  @key_iteration_count = key_iteration_count
13
+ @encrypted_private_key = encrypted_private_key
12
14
  end
13
15
  end
14
16
  end
@@ -11,12 +11,6 @@ module LastPass
11
11
  open blob, username, password
12
12
  end
13
13
 
14
- # Creates a vault from a locally stored blob
15
- def self.open_local blob_filename, username, password
16
- # TODO: read the blob here
17
- raise NotImplementedError
18
- end
19
-
20
14
  # Creates a vault from a blob object
21
15
  def self.open blob, username, password
22
16
  new blob, blob.encryption_key(username, password)
@@ -38,18 +32,21 @@ module LastPass
38
32
  raise InvalidResponseError, "Blob is truncated"
39
33
  end
40
34
 
41
- @accounts = parse_accounts chunks, encryption_key
35
+ private_key = nil
36
+ if blob.encrypted_private_key
37
+ private_key = Parser.parse_private_key blob.encrypted_private_key, encryption_key
38
+ end
39
+
40
+ @accounts = parse_accounts chunks, encryption_key, private_key
42
41
  end
43
42
 
44
43
  def complete? chunks
45
44
  !chunks.empty? && chunks.last.id == "ENDM" && chunks.last.payload == "OK"
46
45
  end
47
46
 
48
- def parse_accounts chunks, encryption_key
47
+ def parse_accounts chunks, encryption_key, private_key
49
48
  accounts = []
50
-
51
49
  key = encryption_key
52
- rsa_private_key = nil
53
50
 
54
51
  chunks.each do |i|
55
52
  case i.id
@@ -59,11 +56,11 @@ module LastPass
59
56
  if account
60
57
  accounts << account
61
58
  end
62
- when "PRIK"
63
- rsa_private_key = Parser.parse_PRIK i, encryption_key
64
59
  when "SHAR"
60
+ raise "private_key must be provided" if !private_key
61
+
65
62
  # After SHAR chunk all the folliwing accounts are enrypted with a new key
66
- key = Parser.parse_SHAR(i, encryption_key, rsa_private_key)[:encryption_key]
63
+ key = Parser.parse_SHAR(i, encryption_key, private_key)[:encryption_key]
67
64
  end
68
65
  end
69
66
 
@@ -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.5.0"
5
+ VERSION = "1.6.0"
6
6
  end
@@ -6,14 +6,16 @@ require "spec_helper"
6
6
  describe LastPass::Blob do
7
7
  let(:bytes) { "TFBBVgAAAAMxMjJQUkVNAAAACjE0MTQ5".decode64 }
8
8
  let(:key_iteration_count) { 500 }
9
+ let(:encrypted_private_key) { "DEADBEEF" }
9
10
  let(:username) { "postlass@gmail.com" }
10
11
  let(:password) { "pl1234567890" }
11
12
  let(:encryption_key) { "OfOUvVnQzB4v49sNh4+PdwIFb9Fr5+jVfWRTf+E2Ghg=".decode64 }
12
13
 
13
- subject { LastPass::Blob.new bytes, key_iteration_count }
14
+ subject { LastPass::Blob.new bytes, key_iteration_count, encrypted_private_key }
14
15
 
15
16
  its(:bytes) { should eq bytes }
16
17
  its(:key_iteration_count) { should eq key_iteration_count }
18
+ its(:encrypted_private_key) { should eq encrypted_private_key }
17
19
 
18
20
  describe "#encryption_key" do
19
21
  it "returns encryption key" do
@@ -10,18 +10,18 @@ describe LastPass::Fetcher do
10
10
 
11
11
  let(:hash) { "7880a04588cfab954aa1a2da98fd9c0d2c6eba4c53e36a94510e6dbf30759256" }
12
12
  let(:session_id) { "53ru,Hb713QnEVM5zWZ16jMvxS0" }
13
- let(:session) { LastPass::Session.new session_id, key_iteration_count }
13
+ let(:session) { LastPass::Session.new session_id, key_iteration_count, "DEADBEEF" }
14
14
 
15
15
  let(:blob_response) { "TFBBVgAAAAMxMjJQUkVNAAAACjE0MTQ5" }
16
16
  let(:blob_bytes) { blob_response.decode64 }
17
- let(:blob) { LastPass::Blob.new blob_bytes, key_iteration_count }
17
+ let(:blob) { LastPass::Blob.new blob_bytes, key_iteration_count, "DEADBEEF" }
18
18
 
19
- let(:login_post_data) { {method: "mobile",
20
- web: 1,
21
- xml: 1,
19
+ let(:login_post_data) { {method: "cli",
20
+ xml: 2,
22
21
  username: username,
23
22
  hash: hash,
24
- iterations: key_iteration_count} }
23
+ iterations: key_iteration_count,
24
+ includeprivatekeyenc: 1} }
25
25
 
26
26
  let(:device_id) { "492378378052455" }
27
27
  let(:login_post_data_with_device_id) { login_post_data.merge({imei: device_id}) }
@@ -36,7 +36,7 @@ describe LastPass::Fetcher do
36
36
  it "makes a GET request" do
37
37
  web_client = double "web_client"
38
38
  expect(web_client).to receive(:get)
39
- .with("https://lastpass.com/logout.php?mobile=1", cookies: {"PHPSESSID" => session_id})
39
+ .with("https://lastpass.com/logout.php?method=cli&noredirect=1", cookies: {"PHPSESSID" => session_id})
40
40
  .and_return(http_ok "")
41
41
  LastPass::Fetcher.logout session, web_client
42
42
  end
@@ -98,7 +98,7 @@ describe LastPass::Fetcher do
98
98
  web_client = double("web_client")
99
99
  expect(web_client).to receive(:post)
100
100
  .with("https://lastpass.com/login.php", format: :xml, body: post_data)
101
- .and_return(http_ok("ok" => {"sessionid" => session_id}))
101
+ .and_return(http_ok("response" => {"ok" => {"sessionid" => session_id, "privatekeyenc" => "DEADBEEF"}}))
102
102
 
103
103
  LastPass::Fetcher.request_login username,
104
104
  password,
@@ -125,7 +125,7 @@ describe LastPass::Fetcher do
125
125
  end
126
126
 
127
127
  it "returns a session" do
128
- expect(request_login_with_xml "<ok sessionid='#{session_id}' />").to eq session
128
+ expect(request_login_with_xml "<response><ok sessionid='#{session_id}' /></response>").to eq session
129
129
  end
130
130
 
131
131
  it "raises an exception on HTTP error" do
@@ -177,7 +177,7 @@ describe LastPass::Fetcher do
177
177
  it "raises an exception on missing/incorrect Yubikey password" do
178
178
  message = "Your account settings have restricted you from logging in " +
179
179
  "from mobile devices that do not support YubiKey authentication."
180
- expect { request_login_with_lastpass_error "yubikeyrestricted", message }
180
+ expect { request_login_with_lastpass_error "otprequired", message }
181
181
  .to raise_error LastPass::LastPassIncorrectYubikeyPasswordError, message
182
182
  end
183
183
 
@@ -197,7 +197,7 @@ describe LastPass::Fetcher do
197
197
  describe ".fetch" do
198
198
  it "makes a GET request" do
199
199
  expect(web_client = double("web_client")).to receive(:get)
200
- .with("https://lastpass.com/getaccts.php?mobile=1&b64=1&hash=0.0&hasplugin=3.0.23&requestsrc=android",
200
+ .with("https://lastpass.com/getaccts.php?mobile=1&b64=1&hash=0.0&hasplugin=3.0.23&requestsrc=cli",
201
201
  format: :plain,
202
202
  cookies: {"PHPSESSID" => session_id})
203
203
  .and_return(http_ok(blob_response))
@@ -6,7 +6,7 @@ require_relative "test_data"
6
6
 
7
7
  describe LastPass::Parser do
8
8
  let(:key_iteration_count) { 5000 }
9
- let(:blob) { LastPass::Blob.new TEST_BLOB, key_iteration_count }
9
+ let(:blob) { LastPass::Blob.new TEST_BLOB, key_iteration_count, "DEADBEEF" }
10
10
  let(:padding) { "BEEFFACE"}
11
11
  let(:encryption_key) { "OfOUvVnQzB4v49sNh4+PdwIFb9Fr5+jVfWRTf+E2Ghg=".decode64 }
12
12
  let(:encoded_rsa_key) { "98F3F5518AE7C03EBBF195A616361619033509FB1FFA0408E883B7C5E80381F8" +
@@ -114,9 +114,10 @@ describe LastPass::Parser do
114
114
  end
115
115
  end
116
116
 
117
- describe ".parse_PRIK" do
118
- let(:chunk) { LastPass::Chunk.new "PRIK", encoded_rsa_key }
119
- let(:rsa_key) { LastPass::Parser.parse_PRIK chunk, rsa_key_encryption_key }
117
+ describe ".parse_private_key" do
118
+ let(:rsa_key) {
119
+ LastPass::Parser.parse_private_key encoded_rsa_key, rsa_key_encryption_key
120
+ }
120
121
 
121
122
  it "parses private key" do
122
123
  expect(rsa_key).to be_instance_of OpenSSL::PKey::RSA
@@ -6,9 +6,11 @@ require "spec_helper"
6
6
  describe LastPass::Session do
7
7
  let(:id) { "53ru,Hb713QnEVM5zWZ16jMvxS0" }
8
8
  let(:key_iteration_count) { 5000 }
9
+ let(:encrypted_private_key) { "DEADBEEF" }
9
10
 
10
- subject { LastPass::Session.new id, key_iteration_count }
11
+ subject { LastPass::Session.new id, key_iteration_count, encrypted_private_key }
11
12
 
12
13
  its(:id) { should eq id }
13
14
  its(:key_iteration_count) { should eq key_iteration_count }
15
+ its(:encrypted_private_key) { should eq encrypted_private_key }
14
16
  end
@@ -6,7 +6,7 @@ require "test_data"
6
6
 
7
7
  describe LastPass::Vault do
8
8
  let(:vault) {
9
- LastPass::Vault.new LastPass::Blob.new(TEST_BLOB, TEST_KEY_ITERATION_COUNT),
9
+ LastPass::Vault.new LastPass::Blob.new(TEST_BLOB, TEST_KEY_ITERATION_COUNT, nil),
10
10
  TEST_ENCRYPTION_KEY
11
11
  }
12
12
 
@@ -15,7 +15,7 @@ describe LastPass::Vault do
15
15
  [1, 2, 3, 4, 5, 10, 100, 1000].each do |i|
16
16
  expect {
17
17
  blob = TEST_BLOB[0..(-1 - i)]
18
- LastPass::Vault.new LastPass::Blob.new(blob, TEST_KEY_ITERATION_COUNT),
18
+ LastPass::Vault.new LastPass::Blob.new(blob, TEST_KEY_ITERATION_COUNT, nil),
19
19
  TEST_ENCRYPTION_KEY
20
20
  }.to raise_error LastPass::InvalidResponseError, "Blob is truncated"
21
21
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lastpass
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.0
4
+ version: 1.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dmitry Yakimenko
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-02-05 00:00:00.000000000 Z
11
+ date: 2019-08-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: httparty
@@ -138,8 +138,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
138
138
  - !ruby/object:Gem::Version
139
139
  version: '0'
140
140
  requirements: []
141
- rubyforge_project:
142
- rubygems_version: 2.5.1
141
+ rubygems_version: 3.0.3
143
142
  signing_key:
144
143
  specification_version: 4
145
144
  summary: Unofficial LastPass API