lastpass 1.5.0 → 1.6.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.
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