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 +5 -5
- data/.travis.yml +3 -0
- data/CHANGELOG.md +7 -0
- data/lib/lastpass/blob.rb +4 -2
- data/lib/lastpass/fetcher.rb +15 -10
- data/lib/lastpass/parser.rb +40 -25
- data/lib/lastpass/session.rb +4 -2
- data/lib/lastpass/vault.rb +10 -13
- data/lib/lastpass/version.rb +1 -1
- data/spec/blob_spec.rb +3 -1
- data/spec/fetcher_spec.rb +11 -11
- data/spec/parser_spec.rb +5 -4
- data/spec/session_spec.rb +3 -1
- data/spec/vault_spec.rb +2 -2
- metadata +3 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 7051161f3612e54ae85ce0930e91973c84fd12d0395c3c4703d18c78151d49c6
|
4
|
+
data.tar.gz: b586d087e2697a0f4f63d3157677bd85b6583828ab124b13d43d4c5dd65a6a0a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b3fff4b432c45316ee806e854d4b95c8c0e7e77671bed0d65ce091a611217bfd90878fd6e995845f3551d9927f0ecc4dbd3fafd88236f82dd6d2f00fd9a4f996
|
7
|
+
data.tar.gz: 32ef67cac6f64a4478210ba82eedf21a34e4e7f3edc1b66601db97473c675b6b662b4d91d0a5e746df2c66f194eb6feb341918528260c3a70d901bf78e4d89a4
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
data/lib/lastpass/blob.rb
CHANGED
@@ -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
|
data/lib/lastpass/fetcher.rb
CHANGED
@@ -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?
|
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=
|
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),
|
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: "
|
54
|
-
|
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
|
-
|
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
|
-
"
|
103
|
+
"otprequired" => LastPassIncorrectYubikeyPasswordError,
|
99
104
|
}
|
100
105
|
|
101
106
|
cause = error["cause"]
|
data/lib/lastpass/parser.rb
CHANGED
@@ -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
|
293
|
+
aes = OpenSSL::Cipher.new "aes-256-#{cipher}"
|
279
294
|
aes.decrypt
|
280
295
|
aes.key = encryption_key
|
281
296
|
aes.iv = iv
|
data/lib/lastpass/session.rb
CHANGED
@@ -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
|
data/lib/lastpass/vault.rb
CHANGED
@@ -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
|
-
|
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,
|
63
|
+
key = Parser.parse_SHAR(i, encryption_key, private_key)[:encryption_key]
|
67
64
|
end
|
68
65
|
end
|
69
66
|
|
data/lib/lastpass/version.rb
CHANGED
data/spec/blob_spec.rb
CHANGED
@@ -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
|
data/spec/fetcher_spec.rb
CHANGED
@@ -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: "
|
20
|
-
|
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?
|
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}'
|
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 "
|
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=
|
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))
|
data/spec/parser_spec.rb
CHANGED
@@ -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 ".
|
118
|
-
let(:
|
119
|
-
|
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
|
data/spec/session_spec.rb
CHANGED
@@ -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
|
data/spec/vault_spec.rb
CHANGED
@@ -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.
|
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:
|
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
|
-
|
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
|