lastpass 1.3.0 → 1.7.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: '09a65355723c0ba7300141aa42b691b35072efe71328f6095dce6580c4ff0ccd'
4
+ data.tar.gz: 3a18f05b3a62ef6fc42568956cc5a99f0bb229597795333363e54fadd036c107
5
+ SHA512:
6
+ metadata.gz: e08bffaa03731f26519611ea40b52aec82097e3421cda2e0b68ebd785fdafaf9e9e9d6bb39ca8ae4c8df606d43e417003032d006e6a8966591a6db1727073328
7
+ data.tar.gz: b49b1995780a93c08b87396e78cb8d6718e0920336cd4981c0421290cfaee653904aaca4af23573aa2458a51a8aeb8d4dc4d1e0775dd20410602bb188b7cd7b4
@@ -0,0 +1,23 @@
1
+ name: Ruby
2
+
3
+ on:
4
+ push:
5
+ branches: [ master ]
6
+ pull_request:
7
+ branches: [ master ]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ fail-fast: false
14
+ matrix:
15
+ ruby: [2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7]
16
+ steps:
17
+ - uses: actions/checkout@v2
18
+ - uses: ruby/setup-ruby@v1
19
+ with:
20
+ ruby-version: ${{ matrix.ruby }}
21
+ bundler-cache: true # runs 'bundle install' and caches installed gems automatically
22
+ - run: bundle install
23
+ - run: bundle exec rake
data/.gitignore CHANGED
@@ -1,4 +1,5 @@
1
- Gemfile.lock
2
- lastpass-*.gem
3
- coverage
4
- example/credentials.yaml
1
+ /pkg/
2
+ /Gemfile.lock
3
+ /coverage
4
+ /example/credentials.yaml
5
+ /vendor/
@@ -1,5 +1,10 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 1.9.3
4
- - 2.0.0
5
- - 2.1.5
3
+ - 2.0.0-p648
4
+ - 2.1.10
5
+ - 2.2.9
6
+ - 2.3.8
7
+ - 2.4.9
8
+ - 2.5.8
9
+ - 2.6.6
10
+ - 2.7.1
@@ -1,3 +1,44 @@
1
+ Version 1.7.0
2
+ -------------
3
+
4
+ - Parse generic secure notes (thanks to Michael Chui @saraid)
5
+ - Parse and store secure notes for accounts (see `Account.note` property)
6
+
7
+ Version 1.6.1
8
+ -------------
9
+
10
+ - Obsolete URI.encode is replaced with URI.encode_www_form_component to
11
+ inhibit the warning
12
+ - Travic CI runs on all 2.* latest minor version releases (2.0.0 to 2.7.1)
13
+
14
+ Version 1.6.0
15
+ -------------
16
+
17
+ - Changed the way the private keys are parsed
18
+ - OpenSSL 1.1.0 support
19
+ - Bug fixes
20
+
21
+ Version 1.5.0
22
+ -------------
23
+
24
+ - Fixed failing `get_iteraction_count`. POST parameters are moved from the
25
+ query (URL) to the body.
26
+ - pbkdf2-ruby gem is no longer used as it was causing problems. Relying on
27
+ built-in `OpenSSL::PKCS5.pbkdf2_hmac`
28
+ - Minimum supported Ruby version is 2.0.0
29
+ - Dependencies updated to their laters versions
30
+ - Travis CI fixed
31
+
32
+ Version 1.4.0
33
+ -------------
34
+
35
+ - Added device id (IMEI/UUID) support
36
+ - Log out after fetching the blob to close the newly open session on LP server
37
+ to prevent triggering anti-hacking logic (hopefully)
38
+ - Verify that the recieved blob is marked with ENDM chunk and hasn't been
39
+ truncated in the process
40
+
41
+
1
42
  Version 1.3.0
2
43
  -------------
3
44
 
data/README.md CHANGED
@@ -1,14 +1,14 @@
1
1
  LastPass Ruby API
2
2
  =================
3
3
 
4
- [![Build Status](https://travis-ci.org/detunized/lastpass-ruby.png?branch=master)](https://travis-ci.org/detunized/lastpass-ruby)
5
- [![Coverage Status](https://coveralls.io/repos/detunized/lastpass-ruby/badge.png?branch=master)](https://coveralls.io/r/detunized/lastpass-ruby?branch=master)
6
- [![Code Climate](https://codeclimate.com/github/detunized/lastpass-ruby.png)](https://codeclimate.com/github/detunized/lastpass-ruby)
7
- [![Dependency Status](https://gemnasium.com/detunized/lastpass-ruby.png)](https://gemnasium.com/detunized/lastpass-ruby)
4
+ [![Build Status](https://travis-ci.org/detunized/lastpass-ruby.svg?branch=master)](https://travis-ci.org/detunized/lastpass-ruby)
5
+ [![Coverage Status](https://coveralls.io/repos/detunized/lastpass-ruby/badge.svg?branch=master)](https://coveralls.io/r/detunized/lastpass-ruby?branch=master)
6
+ [![Code Climate](https://codeclimate.com/github/detunized/lastpass-ruby.svg)](https://codeclimate.com/github/detunized/lastpass-ruby)
7
+ [![Dependency Status](https://gemnasium.com/detunized/lastpass-ruby.svg)](https://gemnasium.com/detunized/lastpass-ruby)
8
8
 
9
9
  **This is unofficial LastPass API.**
10
10
 
11
- There's also [C#/.NET port](https://github.com/detunized/lastpass-sharp) available.
11
+ There are also [a C#/.NET port](https://github.com/detunized/lastpass-sharp) and [a Python port](https://github.com/konomae/lastpass-python) available.
12
12
 
13
13
  This library implements fetching and parsing of LastPass data. The library is
14
14
  still in the proof of concept stage and doesn't support all LastPass features
data/Rakefile CHANGED
@@ -1,6 +1,7 @@
1
1
  # Copyright (C) 2013 Dmitry Yakimenko (detunized@gmail.com).
2
2
  # Licensed under the terms of the MIT license. See LICENCE for details.
3
3
 
4
+ require "bundler/gem_tasks"
4
5
  require "rspec/core/rake_task"
5
6
 
6
7
  task :default => :spec
@@ -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|
@@ -12,17 +12,16 @@ Gem::Specification.new do |s|
12
12
  s.email = "detunized@gmail.com"
13
13
  s.homepage = "https://github.com/detunized/lastpass-ruby"
14
14
  s.summary = "Unofficial LastPass API"
15
- s.description = "Unofficial LastPass API"
15
+ s.description = "Read only access to the online LastPass vault"
16
16
 
17
- s.required_ruby_version = ">= 1.9.3"
17
+ s.required_ruby_version = ">= 2.0.0"
18
18
 
19
- s.add_dependency "httparty", "~> 0.13.0"
20
- s.add_dependency "pbkdf2-ruby", "~> 0.2.0"
19
+ s.add_dependency "httparty", "~> 0.14.0"
21
20
 
22
- s.add_development_dependency "rake", "~> 10.4.0"
23
- s.add_development_dependency "rspec", "~> 3.1.0"
24
- s.add_development_dependency "rspec-its", "~> 1.1.0"
25
- s.add_development_dependency "coveralls", "~> 0.7.0"
21
+ s.add_development_dependency "rake", "~> 12.0"
22
+ s.add_development_dependency "rspec", "~> 3.5"
23
+ s.add_development_dependency "rspec-its", "~> 1.2"
24
+ s.add_development_dependency "coveralls", "~> 0.8.19"
26
25
 
27
26
  s.files = `git ls-files`.split "\n"
28
27
  s.test_files = `git ls-files spec`.split "\n"
@@ -4,7 +4,6 @@
4
4
  require "base64"
5
5
  require "httparty"
6
6
  require "openssl"
7
- require "pbkdf2"
8
7
  require "stringio"
9
8
 
10
9
  require "lastpass/account"
@@ -13,6 +12,7 @@ require "lastpass/chunk"
13
12
  require "lastpass/exceptions"
14
13
  require "lastpass/http"
15
14
  require "lastpass/fetcher"
15
+ require "lastpass/note"
16
16
  require "lastpass/parser"
17
17
  require "lastpass/session"
18
18
  require "lastpass/vault"
@@ -8,14 +8,16 @@ module LastPass
8
8
  :username,
9
9
  :password,
10
10
  :url,
11
+ :notes,
11
12
  :group
12
13
 
13
- def initialize id, name, username, password, url, group
14
+ def initialize id, name, username, password, url, notes, group
14
15
  @id = id
15
16
  @name = name
16
17
  @username = username
17
18
  @password = password
18
19
  @url = url
20
+ @notes = notes
19
21
  @group = group
20
22
  end
21
23
  end
@@ -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
@@ -3,24 +3,33 @@
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?method=cli&noredirect=1",
13
+ cookies: {"PHPSESSID" => URI.encode_www_form_component(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
12
- 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",
13
20
  format: :plain,
14
- cookies: {"PHPSESSID" => URI.encode(session.id)}
21
+ cookies: {"PHPSESSID" => URI.encode_www_form_component(session.id)}
15
22
 
16
23
  raise NetworkError unless response.response.is_a? Net::HTTPOK
17
24
 
18
- 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
19
28
  end
20
29
 
21
30
  def self.request_iteration_count username, web_client = http
22
31
  response = web_client.post "https://lastpass.com/iterations.php",
23
- query: {email: username}
32
+ body: {email: username}
24
33
 
25
34
  raise NetworkError unless response.response.is_a? Net::HTTPOK
26
35
 
@@ -39,18 +48,20 @@ module LastPass
39
48
  password,
40
49
  key_iteration_count,
41
50
  multifactor_password = nil,
51
+ client_id = nil,
42
52
  web_client = http
43
53
 
44
54
  body = {
45
- method: "mobile",
46
- web: 1,
47
- xml: 1,
55
+ method: "cli",
56
+ xml: 2,
48
57
  username: username,
49
58
  hash: make_hash(username, password, key_iteration_count),
50
- iterations: key_iteration_count
59
+ iterations: key_iteration_count,
60
+ includeprivatekeyenc: 1
51
61
  }
52
62
 
53
63
  body[:otp] = multifactor_password if multifactor_password
64
+ body[:imei] = client_id if client_id
54
65
 
55
66
  response = web_client.post "https://lastpass.com/login.php",
56
67
  format: :xml,
@@ -66,11 +77,14 @@ module LastPass
66
77
  end
67
78
 
68
79
  def self.create_session parsed_response, key_iteration_count
69
- ok = parsed_response["ok"]
80
+ ok = (parsed_response["response"] || {})["ok"]
70
81
  if ok.is_a? Hash
71
82
  session_id = ok["sessionid"]
72
83
  if session_id.is_a? String
73
- 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
74
88
  end
75
89
  end
76
90
 
@@ -86,7 +100,7 @@ module LastPass
86
100
  "unknownpassword" => LastPassInvalidPasswordError,
87
101
  "googleauthrequired" => LastPassIncorrectGoogleAuthenticatorCodeError,
88
102
  "googleauthfailed" => LastPassIncorrectGoogleAuthenticatorCodeError,
89
- "yubikeyrestricted" => LastPassIncorrectYubikeyPasswordError,
103
+ "otprequired" => LastPassIncorrectYubikeyPasswordError,
90
104
  }
91
105
 
92
106
  cause = error["cause"]
@@ -108,13 +122,7 @@ module LastPass
108
122
  if key_iteration_count == 1
109
123
  Digest::SHA256.digest username + password
110
124
  else
111
- PBKDF2
112
- .new(password: password,
113
- salt: username,
114
- iterations: key_iteration_count,
115
- key_length: 32)
116
- .bin_string
117
- .force_encoding "BINARY"
125
+ OpenSSL::PKCS5.pbkdf2_hmac password, username, key_iteration_count, 32, "sha256"
118
126
  end
119
127
  end
120
128
 
@@ -122,12 +130,11 @@ module LastPass
122
130
  if key_iteration_count == 1
123
131
  Digest::SHA256.hexdigest Digest.hexencode(make_key(username, password, 1)) + password
124
132
  else
125
- PBKDF2
126
- .new(password: make_key(username, password, key_iteration_count),
127
- salt: password,
128
- iterations: 1,
129
- key_length: 32)
130
- .hex_string
133
+ Digest.hexencode OpenSSL::PKCS5.pbkdf2_hmac make_key(username, password, key_iteration_count),
134
+ password,
135
+ 1,
136
+ 32,
137
+ "sha256"
131
138
  end
132
139
  end
133
140
 
@@ -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
@@ -0,0 +1,15 @@
1
+ module LastPass
2
+ class Note
3
+ attr_reader :id,
4
+ :name,
5
+ :notes,
6
+ :group
7
+
8
+ def initialize id, name, notes, group
9
+ @id = id
10
+ @name = name
11
+ @notes = notes
12
+ @group = group
13
+ end
14
+ end
15
+ end
@@ -7,7 +7,7 @@ module LastPass
7
7
  RSA_PKCS1_OAEP_PADDING = 4
8
8
 
9
9
  # Secure note types that contain account-like information
10
- ALLOWED_SECURE_NOTE_TYPES = {
10
+ ACCOUNT_LIKE_SECURE_NOTE_TYPES = {
11
11
  "Server" => true,
12
12
  "Email Account" => true,
13
13
  "Database" => true,
@@ -28,9 +28,9 @@ module LastPass
28
28
  end
29
29
 
30
30
  # Parses an account chunk, decrypts and creates an Account object.
31
- # May return nil when the chunk does not represent an account.
32
- # All secure notes are ACCTs but not all of them strore account
33
- # information.
31
+ # Returns either an Account or a Note object, in case of a generic
32
+ # note that doesn't represent an account. All secure notes are ACCTs
33
+ # but not all of them store account information.
34
34
  #
35
35
  # TODO: Make a test case that covers secure note account
36
36
  def self.parse_ACCT chunk, encryption_key
@@ -48,44 +48,20 @@ 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
55
- return nil
51
+ parsed = parse_secure_note_server notes
52
+ if !ACCOUNT_LIKE_SECURE_NOTE_TYPES.key? parsed[:type]
53
+ return Note.new id, name, notes, group
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
- Account.new id, name, username, password, url, group
61
+ Account.new id, name, username, password, url, notes, group
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,24 +88,63 @@ 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
- url = nil
117
- username = nil
118
- password = nil
131
+ info = {}
119
132
 
120
133
  notes.split("\n").each do |i|
121
134
  key, value = i.split ":", 2
122
135
  case key
136
+ when "NoteType"
137
+ info[:type] = value
123
138
  when "Hostname"
124
- url = value
139
+ info[:url] = value
125
140
  when "Username"
126
- username = value
141
+ info[:username] = value
127
142
  when "Password"
128
- password = value
143
+ info[:password] = value
129
144
  end
130
145
  end
131
146
 
132
- [url, username, password]
147
+ info
133
148
  end
134
149
 
135
150
  # Reads one chunk from a stream and creates a Chunk object with the data read.
@@ -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