lastpass 1.0.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,14 @@
1
+ # Copyright (C) 2013 Dmitry Yakimenko (detunized@gmail.com).
2
+ # Licensed under the terms of the MIT license. See LICENCE for details.
3
+
4
+ module LastPass
5
+ class Session
6
+ attr_reader :id,
7
+ :key_iteration_count
8
+
9
+ def initialize id, key_iteration_count
10
+ @id = id
11
+ @key_iteration_count = key_iteration_count
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,34 @@
1
+ # Copyright (C) 2013 Dmitry Yakimenko (detunized@gmail.com).
2
+ # Licensed under the terms of the MIT license. See LICENCE for details.
3
+
4
+ module LastPass
5
+ class Vault
6
+ attr_reader :accounts
7
+
8
+ # Fetches a blob from the server and creates a vault
9
+ def self.open_remote username, password
10
+ open Vault.fetch_blob(username, password), username, password
11
+ end
12
+
13
+ # Creates a vault from a locally stored blob
14
+ def self.open_local blob_filename, username, password
15
+ # TODO: read the blob here
16
+ end
17
+
18
+ # Creates a vault from a blob object
19
+ def self.open blob, username, password
20
+ new blob, blob.encryption_key(username, password)
21
+ end
22
+
23
+ # Just fetches the blob, could be used to store it locally
24
+ def self.fetch_blob username, password
25
+ Fetcher.fetch Fetcher.login username, password
26
+ end
27
+
28
+ # This more of an internal method, use of the static constructors instead
29
+ def initialize blob, encryption_key
30
+ chunks = Parser.extract_chunks blob
31
+ @accounts = (chunks["ACCT"] || []).map { |i| Parser.parse_account i, encryption_key }
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,6 @@
1
+ # Copyright (C) 2013 Dmitry Yakimenko (detunized@gmail.com).
2
+ # Licensed under the terms of the MIT license. See LICENCE for details.
3
+
4
+ module LastPass
5
+ VERSION = "1.0.0"
6
+ end
@@ -0,0 +1,22 @@
1
+ # Copyright (C) 2013 Dmitry Yakimenko (detunized@gmail.com).
2
+ # Licensed under the terms of the MIT license. See LICENCE for details.
3
+
4
+ require "spec_helper"
5
+
6
+ describe LastPass::Account do
7
+ let(:id) { "id" }
8
+ let(:name) { "name" }
9
+ let(:username) { "username" }
10
+ let(:password) { "password" }
11
+ let(:url) { "url" }
12
+ let(:group) { "group" }
13
+
14
+ subject { LastPass::Account.new id, name, username, password, url, group }
15
+
16
+ its(:id) { should eq id }
17
+ its(:name) { should eq name }
18
+ its(:username) { should eq username }
19
+ its(:password) { should eq password }
20
+ its(:url) { should eq url }
21
+ its(:group) { should eq group }
22
+ end
@@ -0,0 +1,23 @@
1
+ # Copyright (C) 2013 Dmitry Yakimenko (detunized@gmail.com).
2
+ # Licensed under the terms of the MIT license. See LICENCE for details.
3
+
4
+ require "spec_helper"
5
+
6
+ describe LastPass::Blob do
7
+ let(:bytes) { "TFBBVgAAAAMxMjJQUkVNAAAACjE0MTQ5".decode64 }
8
+ let(:key_iteration_count) { 500 }
9
+ let(:username) { "postlass@gmail.com" }
10
+ let(:password) { "pl1234567890" }
11
+ let(:encryption_key) { "OfOUvVnQzB4v49sNh4+PdwIFb9Fr5+jVfWRTf+E2Ghg=".decode64 }
12
+
13
+ subject { LastPass::Blob.new bytes, key_iteration_count }
14
+
15
+ its(:bytes) { should eq bytes }
16
+ its(:key_iteration_count) { should eq key_iteration_count }
17
+
18
+ describe "#encryption_key" do
19
+ it "returns encryption key" do
20
+ expect(subject.encryption_key username, password).to eq encryption_key
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,14 @@
1
+ # Copyright (C) 2013 Dmitry Yakimenko (detunized@gmail.com).
2
+ # Licensed under the terms of the MIT license. See LICENCE for details.
3
+
4
+ require "spec_helper"
5
+
6
+ describe LastPass::Chunk do
7
+ let(:id) { "IDID" }
8
+ let(:payload) { "Payload" }
9
+
10
+ subject { LastPass::Chunk.new id, payload }
11
+
12
+ its(:id) { should eq id }
13
+ its(:payload) { should eq payload }
14
+ end
@@ -0,0 +1,228 @@
1
+ # Copyright (C) 2013 Dmitry Yakimenko (detunized@gmail.com).
2
+ # Licensed under the terms of the MIT license. See LICENCE for details.
3
+
4
+ require "spec_helper"
5
+
6
+ describe LastPass::Fetcher do
7
+ let(:username) { "username" }
8
+ let(:password) { "password" }
9
+ let(:key_iteration_count) { 5000 }
10
+ let(:session_id) { "53ru,Hb713QnEVM5zWZ16jMvxS0" }
11
+ let(:session) { LastPass::Session.new session_id, key_iteration_count }
12
+ let(:blob_response) { "TFBBVgAAAAMxMjJQUkVNAAAACjE0MTQ5" }
13
+ let(:blob_bytes) { blob_response.decode64 }
14
+ let(:blob) { LastPass::Blob.new blob_bytes, key_iteration_count }
15
+
16
+ describe ".request_iteration_count" do
17
+ it "makes a POST request" do
18
+ expect(web_client = double("web_client")).to receive(:post)
19
+ .with("https://lastpass.com/iterations.php", query: {email: username})
20
+ .and_return(http_ok(key_iteration_count.to_s))
21
+
22
+ LastPass::Fetcher.request_iteration_count username, web_client
23
+ end
24
+
25
+ it "returns key iteration count" do
26
+ expect(
27
+ LastPass::Fetcher.request_iteration_count username,
28
+ double("web_client", post: http_ok(key_iteration_count.to_s))
29
+ ).to eq key_iteration_count
30
+ end
31
+
32
+ it "raises an exception on HTTP error" do
33
+ expect {
34
+ LastPass::Fetcher.request_iteration_count username,
35
+ double("web_client", post: http_error)
36
+ }.to raise_error LastPass::NetworkError
37
+ end
38
+
39
+ it "raises an exception on invalid key iteration count" do
40
+ expect {
41
+ LastPass::Fetcher.request_iteration_count username,
42
+ double("web_client", post: http_ok("not a number"))
43
+ }.to raise_error LastPass::InvalidResponse, "Key iteration count is invalid"
44
+ end
45
+
46
+ it "raises an exception on zero key iteration count" do
47
+ expect {
48
+ LastPass::Fetcher.request_iteration_count username,
49
+ double("web_client", post: http_ok("0"))
50
+ }.to raise_error LastPass::InvalidResponse, "Key iteration count is not positive"
51
+ end
52
+
53
+ it "raises an exception on negative key iteration count" do
54
+ expect {
55
+ LastPass::Fetcher.request_iteration_count username,
56
+ double("web_client", post: http_ok("-1"))
57
+ }.to raise_error LastPass::InvalidResponse, "Key iteration count is not positive"
58
+ end
59
+ end
60
+
61
+ describe ".request_login" do
62
+ it "makes a POST request" do
63
+ expect(web_client = double("web_client")).to receive(:post)
64
+ .with("https://lastpass.com/login.php", format: :xml, body: anything)
65
+ .and_return(http_ok("ok" => {"sessionid" => session_id}))
66
+
67
+ LastPass::Fetcher.request_login username, password, key_iteration_count, web_client
68
+ end
69
+
70
+ it "returns a session" do
71
+ expect(request_login_with_xml "<ok sessionid='#{session_id}' />").to eq session
72
+ end
73
+
74
+ it "raises an exception on HTTP error" do
75
+ expect { request_login_with_error }.to raise_error LastPass::NetworkError
76
+ end
77
+
78
+ it "raises an exception when response is not a hash" do
79
+ expect { request_login_with_ok "not a hash" }.to raise_error LastPass::InvalidResponse
80
+ end
81
+
82
+ it "raises an exception on unknown response schema" do
83
+ expect { request_login_with_xml "<unknown />" }.to raise_error LastPass::UnknownResponseSchema
84
+ end
85
+
86
+ it "raises an exception on unknown response schema" do
87
+ expect { request_login_with_xml "<response />" }.to raise_error LastPass::UnknownResponseSchema
88
+ end
89
+
90
+ it "raises an exception on unknown response schema" do
91
+ expect { request_login_with_xml "<response><error /></response>" }
92
+ .to raise_error LastPass::UnknownResponseSchema
93
+ end
94
+
95
+ it "raises an exception on unknown username" do
96
+ message = "Unknown email address."
97
+ expect { request_login_with_lastpass_error "unknownemail", message }
98
+ .to raise_error LastPass::LastPassUnknownUsername, message
99
+ end
100
+
101
+ it "raises an exception on invalid password" do
102
+ message = "Invalid password!"
103
+ expect { request_login_with_lastpass_error "unknownpassword", message }
104
+ .to raise_error LastPass::LastPassInvalidPassword, message
105
+ end
106
+
107
+ it "raises an exception on unknown LastPass error with a message" do
108
+ message = "Unknow error message"
109
+ expect { request_login_with_lastpass_error "Unknown cause", message }
110
+ .to raise_error LastPass::LastPassUnknownError, message
111
+ end
112
+
113
+ it "raises an exception on unknown LastPass error without a message" do
114
+ cause = "Unknown casue"
115
+ expect { request_login_with_lastpass_error cause }
116
+ .to raise_error LastPass::LastPassUnknownError, cause
117
+ end
118
+ end
119
+
120
+ describe ".fetch" do
121
+ it "makes a GET request" do
122
+ expect(web_client = double("web_client")).to receive(:get)
123
+ .with("https://lastpass.com/getaccts.php?mobile=1&b64=1&hash=0.0",
124
+ format: :plain,
125
+ cookies: {"PHPSESSID" => session_id})
126
+ .and_return(http_ok(blob_response))
127
+
128
+ LastPass::Fetcher.fetch session, web_client
129
+ end
130
+
131
+ it "returns a blob" do
132
+ expect(LastPass::Fetcher.fetch session, double("web_client", get: http_ok(blob_response)))
133
+ .to eq blob
134
+ end
135
+
136
+ it "raises an exception on HTTP error" do
137
+ expect {
138
+ LastPass::Fetcher.fetch session, double("web_client", get: http_error)
139
+ } .to raise_error LastPass::NetworkError
140
+ end
141
+ end
142
+
143
+ describe ".make_key" do
144
+ it "generates correct keys" do
145
+ def key iterations
146
+ LastPass::Fetcher.make_key "postlass@gmail.com", "pl1234567890", iterations
147
+ end
148
+
149
+ expect(key 1).to eq "C/Bh2SGWxI8JDu54DbbpV8J9wa6pKbesIb9MAXkeF3Y=".decode64
150
+ expect(key 5).to eq "pE9goazSCRqnWwcixWM4NHJjWMvB5T15dMhe6ug1pZg=".decode64
151
+ expect(key 10).to eq "n9S0SyJdrMegeBHtkxUx8Lzc7wI6aGl+y3/udGmVey8=".decode64
152
+ expect(key 50).to eq "GwI8/kNy1NjIfe3Z0VAZfF78938UVuCi6xAL3MJBux0=".decode64
153
+ expect(key 100).to eq "piGdSULeHMWiBS3QJNM46M5PIYwQXA6cNS10pLB3Xf8=".decode64
154
+ expect(key 500).to eq "OfOUvVnQzB4v49sNh4+PdwIFb9Fr5+jVfWRTf+E2Ghg=".decode64
155
+ expect(key 1000).to eq "z7CdwlIkbu0XvcB7oQIpnlqwNGemdrGTBmDKnL9taPg=".decode64
156
+ end
157
+ end
158
+
159
+ describe ".make_hash" do
160
+ it "generates correct hashes" do
161
+ def hash iterations
162
+ LastPass::Fetcher.make_hash "postlass@gmail.com", "pl1234567890", iterations
163
+ end
164
+
165
+ expect(hash 1).to eq "a1943cfbb75e37b129bbf78b9baeab4ae6dd08225776397f66b8e0c7a913a055"
166
+ expect(hash 5).to eq "a95849e029a7791cfc4503eed9ec96ab8675c4a7c4e82b00553ddd179b3d8445"
167
+ expect(hash 10).to eq "0da0b44f5e6b7306f14e92de6d629446370d05afeb1dc07cfcbe25f169170c16"
168
+ expect(hash 50).to eq "1d5bc0d636da4ad469cefe56c42c2ff71589facb9c83f08fcf7711a7891cc159"
169
+ expect(hash 100).to eq "82fc12024acb618878ba231a9948c49c6f46e30b5a09c11d87f6d3338babacb5"
170
+ expect(hash 500).to eq "3139861ae962801b59fc41ff7eeb11f84ca56d810ab490f0d8c89d9d9ab07aa6"
171
+ expect(hash 1000).to eq "03161354566c396fcd624a424164160e890e96b4b5fa6d942fc6377ab613513b"
172
+ end
173
+ end
174
+
175
+ #
176
+ # Helpers
177
+ #
178
+
179
+ private
180
+
181
+ def mock_response type, code, body
182
+ double response: type.new("1.1", code, ""),
183
+ parsed_response: body
184
+ end
185
+
186
+ def http_ok body
187
+ mock_response Net::HTTPOK, 200, body
188
+ end
189
+
190
+ def http_error body = ""
191
+ mock_response Net::HTTPNotFound, 404, body
192
+ end
193
+
194
+ def xml text
195
+ MultiXml.parse text
196
+ end
197
+
198
+ def lastpass_error cause, message
199
+ if message
200
+ %Q{<response><error cause="#{cause}" message="#{message}" /></response>}
201
+ else
202
+ %Q{<response><error cause="#{cause}" /></response>}
203
+ end
204
+ end
205
+
206
+ def request_login_with_lastpass_error cause, message = nil
207
+ request_login_with_xml lastpass_error cause, message
208
+ end
209
+
210
+ def request_login_with_xml text
211
+ request_login_with_ok xml text
212
+ end
213
+
214
+ def request_login_with_ok response
215
+ request_login_with_response http_ok response
216
+ end
217
+
218
+ def request_login_with_error
219
+ request_login_with_response http_error
220
+ end
221
+
222
+ def request_login_with_response response
223
+ LastPass::Fetcher.request_login username,
224
+ password,
225
+ key_iteration_count,
226
+ double("web_client", post: response)
227
+ end
228
+ end
@@ -0,0 +1,292 @@
1
+ # Copyright (C) 2013 Dmitry Yakimenko (detunized@gmail.com).
2
+ # Licensed under the terms of the MIT license. See LICENCE for details.
3
+
4
+ require "spec_helper"
5
+ require_relative "test_data"
6
+
7
+ describe LastPass::Parser do
8
+ let(:key_iteration_count) { 5000 }
9
+ let(:blob) { LastPass::Blob.new TEST_BLOB, key_iteration_count }
10
+ let(:padding) { "BEEFFACE"}
11
+ let(:encryption_key) { "OfOUvVnQzB4v49sNh4+PdwIFb9Fr5+jVfWRTf+E2Ghg=".decode64 }
12
+
13
+ describe ".extract_chunks" do
14
+ context "returned chunks" do
15
+ let(:chunks) { LastPass::Parser.extract_chunks blob }
16
+
17
+ it { expect(chunks).to be_instance_of Hash }
18
+
19
+ it "all keys are strings" do
20
+ expect(chunks.keys).to match_array TEST_CHUNK_IDS
21
+ end
22
+
23
+ it "all values are arrays" do
24
+ expect(chunks.values.map(&:class).uniq).to eq [Array]
25
+ end
26
+
27
+ it "all arrays contain only chunks" do
28
+ expect(chunks.values.flat_map { |i| i.map &:class }.uniq).to eq [LastPass::Chunk]
29
+ end
30
+
31
+ it "all chunks grouped under correct IDs" do
32
+ expect(
33
+ chunks.all? { |id, chunk_group| chunk_group.map(&:id).uniq == [id] }
34
+ ).to be_true
35
+ end
36
+ end
37
+ end
38
+
39
+ describe ".parse_account" do
40
+ let(:accounts) {
41
+ LastPass::Parser
42
+ .extract_chunks(blob)["ACCT"]
43
+ .map { |i| LastPass::Parser.parse_account i, TEST_ENCRYPTION_KEY }
44
+ }
45
+
46
+ it "parses accounts" do
47
+ expect(accounts.map &:id).to eq TEST_ACCOUNTS.map &:id
48
+ end
49
+ end
50
+
51
+ describe ".read_chunk" do
52
+ it "returns a chunk" do
53
+ with_hex "4142434400000004DEADBEEF" + padding do |io|
54
+ expect(LastPass::Parser.read_chunk io).to eq LastPass::Chunk.new("ABCD", "DEADBEEF".decode_hex)
55
+ expect(io.pos).to eq 12
56
+ end
57
+ end
58
+ end
59
+
60
+ describe ".read_item" do
61
+ it "returns an item" do
62
+ with_hex "00000004DEADBEEF" + padding do |io|
63
+ expect(LastPass::Parser.read_item io).to eq "DEADBEEF".decode_hex
64
+ expect(io.pos).to eq 8
65
+ end
66
+ end
67
+ end
68
+
69
+ describe ".skip_item" do
70
+ it "skips an empty item" do
71
+ with_hex "00000000" + padding do |io|
72
+ LastPass::Parser.skip_item io
73
+ expect(io.pos).to eq 4
74
+ end
75
+ end
76
+
77
+ it "skips a non-empty item" do
78
+ with_hex "00000004DEADBEEF" + padding do |io|
79
+ LastPass::Parser.skip_item io
80
+ expect(io.pos).to eq 8
81
+ end
82
+ end
83
+ end
84
+
85
+ describe ".read_id" do
86
+ it "returns an id" do
87
+ with_bytes "ABCD" + padding do |io|
88
+ expect(LastPass::Parser.read_id io).to eq "ABCD"
89
+ expect(io.pos).to eq 4
90
+ end
91
+ end
92
+ end
93
+
94
+ describe ".read_size" do
95
+ it "returns a size" do
96
+ with_hex "DEADBEEF" + padding do |io|
97
+ expect(LastPass::Parser.read_size io).to eq 0xDEADBEEF
98
+ expect(io.pos).to eq 4
99
+ end
100
+ end
101
+ end
102
+
103
+ describe ".read_payload" do
104
+ it "returns a payload" do
105
+ with_hex "FEEDDEADBEEF" + padding do |io|
106
+ expect(LastPass::Parser.read_payload io, 6).to eq "FEEDDEADBEEF".decode_hex
107
+ expect(io.pos).to eq 6
108
+ end
109
+ end
110
+ end
111
+
112
+ describe ".read_uint32" do
113
+ it "returns a number" do
114
+ with_hex "DEADBEEF" + padding do |io|
115
+ expect(LastPass::Parser.read_size io).to eq 0xDEADBEEF
116
+ expect(io.pos).to eq 4
117
+ end
118
+ end
119
+ end
120
+
121
+ describe ".decode_hex" do
122
+ it "decodes hex" do
123
+ expect(LastPass::Parser.decode_hex "")
124
+ .to eq ""
125
+
126
+ expect(LastPass::Parser.decode_hex "00ff")
127
+ .to eq "\x00\xFF"
128
+
129
+ expect(LastPass::Parser.decode_hex "00010203040506070809")
130
+ .to eq "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09"
131
+
132
+ expect(LastPass::Parser.decode_hex "000102030405060708090a0b0c0d0e0f")
133
+ .to eq "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F"
134
+
135
+ expect(LastPass::Parser.decode_hex "8af633933e96a3c3550c2734bd814195")
136
+ .to eq "\x8A\xF6\x33\x93\x3E\x96\xA3\xC3\x55\x0C\x27\x34\xBD\x81\x41\x95"
137
+ end
138
+
139
+ it "raises exception on odd length" do
140
+ expect { LastPass::Parser.decode_hex "0" }
141
+ .to raise_error ArgumentError, "Input length must be multple of 2"
142
+ end
143
+
144
+ it "raises exception on invalid characters" do
145
+ expect { LastPass::Parser.decode_hex "xz" }
146
+ .to raise_error ArgumentError, "Input contains invalid characters"
147
+ end
148
+ end
149
+
150
+ describe ".decode_base64" do
151
+ it "decodes base64" do
152
+ def check base64, plain
153
+ expect(LastPass::Parser.decode_base64 base64).to eq plain
154
+ end
155
+
156
+ check "", ""
157
+ check "YQ==", "a"
158
+ check "YWI=", "ab"
159
+ check "YWJj", "abc"
160
+ check "YWJjZA==", "abcd"
161
+ end
162
+ end
163
+
164
+ describe ".decode_aes256_auto" do
165
+ def check encoded, decoded
166
+ expect(LastPass::Parser.decode_aes256_auto encoded, encryption_key)
167
+ .to eq decoded
168
+ end
169
+
170
+ it "decodes a blank string" do
171
+ check "", ""
172
+ end
173
+
174
+ it "decodes ECB/plain string" do
175
+ check "BNhd3Q3ZVODxk9c0C788NUPTIfYnZuxXfkghtMJ8jVM=".decode64,
176
+ "All your base are belong to us"
177
+ end
178
+
179
+ it "decodes ECB/base64 string" do
180
+ check "BNhd3Q3ZVODxk9c0C788NUPTIfYnZuxXfkghtMJ8jVM=",
181
+ "All your base are belong to us"
182
+ end
183
+
184
+ it "decodes CBC/plain string" do
185
+ check "IcokDWmjOkKtLpZehWKL6666Uj6fNXPpX6lLWlou+1Lrwb+D3ymP6BAwd6C0TB3hSA==".decode64,
186
+ "All your base are belong to us"
187
+ end
188
+
189
+ it "decodes CBC/base64 string" do
190
+ check "!YFuiAVZgOD2K+s6y8yaMOw==|TZ1+if9ofqRKTatyUaOnfudletslMJ/RZyUwJuR/+aI=",
191
+ "All your base are belong to us"
192
+ end
193
+ end
194
+
195
+ describe ".decode_aes256_ecb_plain" do
196
+ def check encoded, decoded
197
+ expect(LastPass::Parser.decode_aes256_ecb_plain encoded.decode64, encryption_key)
198
+ .to eq decoded
199
+ end
200
+
201
+ it "decodes a blank string" do
202
+ check "", ""
203
+ end
204
+
205
+ it "decodes a short string" do
206
+ check "8mHxIA8rul6eq72a/Gq2iw==", "0123456789"
207
+ end
208
+
209
+ it "decodes a long string" do
210
+ check "BNhd3Q3ZVODxk9c0C788NUPTIfYnZuxXfkghtMJ8jVM=", "All your base are belong to us"
211
+ end
212
+ end
213
+
214
+ describe ".decode_aes256_ecb_base64" do
215
+ def check encoded, decoded
216
+ expect(LastPass::Parser.decode_aes256_ecb_base64 encoded, encryption_key)
217
+ .to eq decoded
218
+ end
219
+
220
+ it "decodes a blank string" do
221
+ check "", ""
222
+ end
223
+
224
+ it "decodes a short string" do
225
+ check "8mHxIA8rul6eq72a/Gq2iw==", "0123456789"
226
+ end
227
+
228
+ it "decodes a long string" do
229
+ check "BNhd3Q3ZVODxk9c0C788NUPTIfYnZuxXfkghtMJ8jVM=", "All your base are belong to us"
230
+ end
231
+ end
232
+
233
+ describe ".decode_aes256_cbc_plain" do
234
+ def check encoded, decoded
235
+ expect(LastPass::Parser.decode_aes256_cbc_plain encoded.decode64, encryption_key)
236
+ .to eq decoded
237
+ end
238
+
239
+ it "decodes a blank string" do
240
+ check "", ""
241
+ end
242
+
243
+ it "decodes a short string" do
244
+ check "IQ+hiIy0vGG4srsHmXChe3ehWc/rYPnfiyqOG8h78DdX", "0123456789"
245
+ end
246
+
247
+ it "decodes a long string" do
248
+ check "IcokDWmjOkKtLpZehWKL6666Uj6fNXPpX6lLWlou+1Lrwb+D3ymP6BAwd6C0TB3hSA==",
249
+ "All your base are belong to us"
250
+ end
251
+ end
252
+
253
+ describe ".decode_aes256_cbc_base64" do
254
+ def check encoded, decoded
255
+ expect(LastPass::Parser.decode_aes256_cbc_base64 encoded, encryption_key)
256
+ .to eq decoded
257
+ end
258
+
259
+ it "decodes a blank string" do
260
+ check "", ""
261
+ end
262
+
263
+ it "decodes a short string" do
264
+ check "!6TZb9bbrqpocMaNgFjrhjw==|f7RcJ7UowesqGk+um+P5ug==", "0123456789"
265
+ end
266
+
267
+ it "decodes a long string" do
268
+ check "!YFuiAVZgOD2K+s6y8yaMOw==|TZ1+if9ofqRKTatyUaOnfudletslMJ/RZyUwJuR/+aI=",
269
+ "All your base are belong to us"
270
+ end
271
+ end
272
+
273
+ #
274
+ # Helpers
275
+ #
276
+
277
+ private
278
+
279
+ def with_blob &block
280
+ with_bytes TEST_BLOB, &block
281
+ end
282
+
283
+ def with_hex hex, &block
284
+ with_bytes hex.decode_hex, &block
285
+ end
286
+
287
+ def with_bytes bytes, &block
288
+ StringIO.open bytes do |io|
289
+ yield io
290
+ end
291
+ end
292
+ end