lastpass 1.3.0 → 1.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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