├── .shellcheckrc
├── LICENSE
├── README.md
├── conf
├── examples
│ └── ex01.ini
├── test-invalid-01
│ ├── ex01.ini
│ └── invalid01.ini
├── test-invalid-02
│ ├── ex01.ini
│ └── invalid02.ini
├── test-invalid-03
│ └── invalid03.ini
├── test-invalid-04
│ ├── ex01.ini
│ └── invalid04.ini
├── test-invalid-05
│ └── invalid05.ini
├── test-invalid-06
│ └── invalid06.ini
├── test-start-01
│ └── NOTE.txt
├── test-valid-01
│ ├── ex01.ini
│ └── valid01.ini
├── test-valid-02
│ ├── ex01.ini
│ └── valid02.ini
└── test-valid-03
│ ├── ex01.ini
│ └── valid03.ini
├── qcrypt
├── qcryptd
└── tests
├── fixtures
├── 1layer01
│ ├── container
│ └── keys
│ │ └── target
├── 2layer01
│ ├── container
│ └── keys
│ │ ├── middle
│ │ └── target
└── loopdev
│ ├── NOTE.txt
│ ├── keys
│ └── target
│ └── loopfile
├── qcrypt.bats
├── qcryptd.bats
├── shellcheck.bats
└── test_common.bash
/.shellcheckrc:
--------------------------------------------------------------------------------
1 | # shellcheck config file
2 |
3 | enable=add-default-case
4 | enable=avoid-nullary-conditions
5 | enable=deprecate-which
6 | enable=quote-safe-variables
7 |
8 | #loads of F/Ps (mostly wrt ret variable):
9 | disable=SC2155
10 | disable=SC2178
11 | disable=SC2128
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 |
635 | Copyright (C)
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | Copyright (C)
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
675 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # qcrypt
2 |
3 | qcrypt is a multilayer encryption tool for [Qubes OS](https://www.qubes-os.org/).
4 |
5 | Each layer is decrypted inside a dedicated destination Virtual Machine (VM)/Qube until the final target VM can decrypt to the plaintext. The source VM doesn't decrypt anything itself.
6 |
7 | It enables you to attach trusted storage to [Qubes OS](https://www.qubes-os.org/) VMs from untrusted storage backends (USB drive, cloud, ...).
8 |
9 | Depending on the setup, it can mitigate attacks stemming from file system parsing bugs and can prevent compromised VMs from leaking their data with incorrect encryption. Header or encryption parsing bugs in [cryptsetup](https://gitlab.com/cryptsetup/cryptsetup/wikis/home) remain an issue, but their impact can be lessened manually (plain mode & mix different encryption algorithms).
10 |
11 | Internally, qcrypt uses [cryptsetup](https://gitlab.com/cryptsetup/cryptsetup/wikis/home) in combination with standard [Qubes OS](https://www.qubes-os.org/) functionality (block device attachments).
12 |
13 | Both qcrypt and qcryptd need to run in [Qubes OS](https://www.qubes-os.org/) dom0.
14 |
15 | # qcryptd
16 |
17 | qcryptd is a daemon to automate `qcrypt` for everyday usage.
18 |
19 | It can detect device attachments and VMs being started and instantly attaches the configured qcrypt storage to the VM which was just started. This way it brings back the "plug & play feeling" for external storage that users are accustomed to from other operating systems.
20 |
21 | # Example use cases
22 |
23 | - slice a large cloud storage pool or USB pen drive into dedicated per-VM storage volumes
24 | - confidentially share data across multiple Qubes OS machines
25 | - bind persistent trusted storage to a disposable VM directory ("limited persistence")
26 |
27 | ## Table of contents
28 |
29 | - [Installation](#installation)
30 | - [A word of caution](#a-word-of-caution)
31 | - [Usage](#usage)
32 | - [qcrypt](#qcrypt)
33 | - [qcryptd](#qcryptd)
34 | - [But I want to use passwords?!](#but-i-want-to-use-passwords?!)
35 | - [Uninstall](#uninstall)
36 | - [Copyright](#copyright)
37 |
38 | ## Installation
39 |
40 | 1. Download [blib](https://github.com/3hhh/blib), copy it to dom0 and install it according to [its instructions](https://github.com/3hhh/blib#installation).
41 | 2. Download this repository with `git clone https://github.com/3hhh/qcrypt.git` or your browser and copy it to dom0.
42 | 3. Move the repository to a directory of your liking.
43 | 4. Symlink the `qcrypt` and `qcryptd` binaries into your dom0 `PATH` for convenience, e.g. to `/usr/bin/`.
44 |
45 | ### A word of caution
46 |
47 | It is recommended to apply standard operational security practices during installation such as:
48 |
49 | - Github SSL certificate checks
50 | - Check the GPG commit signatures using `git log --pretty="format:%h %G? %GK %aN %s"`. All of them should be good (G) signatures coming from the same key `(1533 C122 5C1B 41AF C46B 33EB) EB03 A691 DB2F 0833` (assuming you trust that key).
51 | - Code review
52 |
53 | You're installing something to dom0 after all.
54 |
55 | ## Usage
56 |
57 | ### qcrypt
58 |
59 | `qcrypt luksInit` can be used to create new chains whose content is stored in encrypted form inside the source file in the respective source VM. The initial creation however happens in dom0; keys and the encrypted container are automatically passed to the respective VMs in the chain.
60 | **Warning**: Keep a backup of all encryption keys in the chain unless you're ready to lose your encrypted data.
61 |
62 | Chains can then be opened via `qcrypt open` and their current attachment state can be observed with `qcrypt status`. Without command-line arguments, the latter also provides an overview of all currently active qcrypt chains.
63 | **Warning**: Unexpected shutdowns of VMs belonging to a chain may lead to data loss under extreme circumstances. In practice this rarely happens, but you should be prepared and have a backup available.
64 |
65 | `qcrypt close` will let you close currently active chains.
66 |
67 | Please consult `qcrypt help` after the installation for further details.
68 |
69 | #### Examples
70 |
71 | ```
72 | sudo qcrypt -a --size 3G --wd ~/qcrypt.tmp/ --bak ~/qcrypt.keys/ luksInit sys-usb /home/user/encrypted.lks secret.key mediator-vm work-vm
73 | ```
74 |
75 | *Explanation:*
76 | Autostart all required VMs and create a 3 Gigabyte container inside the `sys-usb` VM at `/home/user/encrypted.lks` (make sure you `sys-usb` has enough empty disk space). The encryption keys shall be named `secret.key` (you'll find two different keys with the same name inside `~/.qcrypt/keys/` in the `mediator-vm` as well as the `work-vm`), the first layer of decryption happen inside the `mediator-vm` and the second inside the `work-vm`. Only the `work-vm` is meant to see the plaintext data.
77 | Moreover create a backup of all involved keys in dom0 inside the `~/qcrypt.keys/` directory and use `~/qcrypt.tmp/` to generate the encryption container and keys in dom0.
78 | The current example has two destination VMs, but leaving out the `mediator-vm` can be appropriate - it depends on your threat model. Please consult `qcrypt help` after the installation for further explanations.
79 |
80 | ```
81 | qcrypt --mp /mnt/ open sys-usb /home/user/encrypted.lks secret.key mediator-vm work-vm
82 | ```
83 |
84 | *Explanation:*
85 | Open the just created container and mount it to `/mnt/` inside the `work-vm`.
86 |
87 | ```
88 | qcrypt status sys-usb /home/user/encrypted.lks secret.key mediator-vm work-vm
89 | ```
90 |
91 | *Explanation:*
92 | Double-check the current state of that chain. In particular a straightforward `qcrypt status` shows an overview of all currently open chains.
93 |
94 | ```
95 | qcrypt close sys-usb /home/user/encrypted.lks secret.key mediator-vm work-vm
96 | ```
97 |
98 | *Explanation:*
99 | Close the chain. Please note that shutting down the `work-vm` without a close should be fine (you might have to use `qcrypt --force close` later on, which may shut down the `mediator-vm`), but shutting down the `mediator-vm` during the attachment is likely to leave your Qubes OS in a dreary state and will probably require you to restart the system.
100 |
101 | ### qcryptd
102 |
103 | In order to manage new chains initialized with `qcrypt luksInit` or previously unmanaged chains with `qcryptd`, you'll have to create one ini configuration file per chain inside `/etc/qcryptd/default`. An example ini file can be found [here](https://github.com/3hhh/qcrypt/blob/master/conf/examples/ex01.ini).
104 |
105 | It is then recommended to check that configuration with `qcryptd check`. Assuming your configuration was found to be correct, you can start the qcryptd service with `qcryptd start` and further control it with `qcryptd stop` and `qcryptd restart`. After configuration file changes it is recommended to do a `qcryptd -c restart`.
106 |
107 | Also see `qcryptd help` for a more detailed description after the installation.
108 |
109 | #### Example
110 |
111 | Assuming that the `/home/user/encrypted.lks` file inside the `sys-usb` VM from the qcrypt example above was put on an external device `/dev/disk/by-uuid/id`, a minimal configuration file to automatically mount the container to the `work-vm` would look as follows:
112 |
113 | ```
114 | source vm=sys-usb
115 | source device=/dev/disk/by-uuid/id
116 | source mount point=/mnt-id-dev
117 | source file=/encrypted.lks
118 | key=secret.key
119 | destination vm 1=mediator-vm
120 | destination vm 2=work-vm
121 | destination mount point=/mnt
122 | read-only=false
123 | ```
124 |
125 | One could put that configuration e.g. inside the directory `/etc/qcryptd/example/med-work.ini` and could then start qcryptd with `qcryptd start example`, which will execute _all_ chain configurations found inside the `/etc/qcryptd/example/` directory.
126 |
127 | Please keep in mind that the `mediator-vm` may be started or stopped by qcryptd at will. So it is highly recommended to use it only for the purpose of that single chain!
128 |
129 | ### But I want to use passwords?!
130 |
131 | Both `qcrypt` and `qcryptd` support storing and retrieving your keys from a [blib](https://github.com/3hhh/blib) key store, which needs to be opened with exactly one password per boot session. Essentially it's just a password-protected luks container holding all your keys.
132 |
133 | Of course, if you ever lose that container, your keys or forget your password, you lose all of your data. So make sure to always have a backup!
134 |
135 | Also make sure _not_ to put key file backups inside a qcrypt container...
136 |
137 | ## Uninstall
138 |
139 | 1. Remove all symlinks that you created during the installation.
140 | 2. Remove the repository clone from dom0.
141 | 3. Uninstall [blib](https://github.com/3hhh/blib) according to [its instructions](https://github.com/3hhh/blib#uninstall).
142 |
143 | ## Copyright
144 |
145 | © 2020 David Hobach
146 | GPLv3
147 |
148 | See `LICENSE` for details.
149 |
--------------------------------------------------------------------------------
/conf/examples/ex01.ini:
--------------------------------------------------------------------------------
1 | #
2 | # A qcryptd configuration example for a single qcrypt chain. Each chain must be configured in a separate
3 | # configuration file.
4 | #
5 | # qcryptd will manage all chains which reside in the same 'target' folder specified during the qcryptd
6 | # start operation.
7 | #
8 |
9 | # VM where the encrypted container will be found once the source device is available (required).
10 | source vm=sys-usb
11 |
12 | # Block device on which the luks container resides (optional). It is recommended to specify it with a unique
13 | # identifier, e.g. by UUID. qcryptd will wait for this device and all necessary VMs to become available before
14 | # attempting to start the qcrypt chain.
15 | #
16 | # Not specifying any device will cause qcryptd to assume that the source file is always available in the root
17 | # file system (/).
18 | source device=/dev/disk/by-uuid/8c3663c5-b9345-6381-9a67-dd813eb12863
19 |
20 | # Mount point to use for the source device inside the source VM (optional). Not specifying a mount point will
21 | # cause qcryptd to assume that the source device is always available on your local file system (/).
22 | #
23 | # qcryptd will enforce that mount point regardless of whether the device is mounted elsewhere already.
24 | source mount point=/mnt-ex01
25 |
26 | # Path relative to the source mount point leading to the encrypted container file inside the source VM (required).
27 | source file=/containers/ex01-container.luks
28 |
29 | # Identifier of the key / file name of the key inside the ~/.qcrypt folder of all involved VMs (required)
30 | key=ex01-key
31 |
32 | # List of destination VMs to use for the chain (at least one is required), key injections (optional) and additio-
33 | # nal cryptsetup options (optional).
34 | # The VM with the highest number is the one that is meant to read the plaintext.
35 | #
36 | # If you need disposable VMs, use named ones and inject the key! E.g.
37 | # `qvm-create --class DispVM --prop netvm='' --template nonet-dvm -l red d-testing`
38 | #
39 | # A single key injection may be specified per destination VM (default: none).
40 | # You need to either specify the full path to the unencrypted key or you can specify a blib key store folder from
41 | # which it can be retrieved, e.g. `keystore://etc/my-keystore`. Use `keystore://` for the default blib key store.
42 | #
43 | # WARNING: Injecting a key into the wrong VM will cause havoc to whatever security guarantees qcrypt provides.
44 | # Therefore make sure to not confuse the below indices!
45 | #
46 | # Note that intermediate VMs (all destination VMs apart from the last; i.e. destination vm 1 here) are auto-
47 | # matically started by qcrypt/qcryptd as needed. They are also shut down automatically during the chain close
48 | # process. So if you use more than a single encryption layer, please make sure to dedicate all of the intermediate
49 | # VMs to that qcrypt chain only. Otherwise the close may impact other chains or whatever else you're doing inside
50 | # that VM.
51 | #
52 | # The options allow to specify cryptsetup parameters for every single VM. They are only relevant for non-luks
53 | # containers as the header contains all relevant information for luks containers (default).
54 | destination vm 1 = d-testing
55 | destination inj 1 = /root/qcrypt-keys/ex01_disp
56 | #destination opt 1 = --type plain --cipher aes-xts-plain64 -s 512 --hash sha512
57 |
58 | destination vm 2 = work
59 | #destination inj 2 = keystore://
60 |
61 | # Mount point of the plain text data in the destination VM (default: not mounted).
62 | destination mount point=/qcrypt-ex01
63 |
64 | # Autostart all VMs required for this chain (default: false).
65 | # By default, only intermediate VMs are automatically started (see above).
66 | #autostart=false
67 |
68 | # Attach the chain in read-only mode (default: true).
69 | read-only=false
70 |
71 | # The chain will not be attempted to be started more than every [startup interval] seconds (default: 300).
72 | # This is meant to prevent repeated startups during error situations. A qcryptd restart can be used to manually
73 | # try again.
74 | #startup interval=5
75 |
76 | # Command to run _before_ a chain open attempt (default: no command).
77 | # A non-zero exit code of this command will prevent the chain from being started.
78 | #pre open command=logger "starting the ex01 chain"
79 |
80 | # Command to run _after_ a successful chain open (default: no command).
81 | #post open command=logger "started the ex01 chain"
82 |
83 | # Command to run _before_ a chain close attempt (default: no command).
84 | # The exit code is ignored.
85 | #pre close command=logger "attempting to close the ex01 chain"
86 |
87 | # Command to run _after_ a successful chain close (default: no command).
88 | #post close command=logger "stopped the ex01 chain"
89 |
--------------------------------------------------------------------------------
/conf/test-invalid-01/ex01.ini:
--------------------------------------------------------------------------------
1 | ../examples/ex01.ini
--------------------------------------------------------------------------------
/conf/test-invalid-01/invalid01.ini:
--------------------------------------------------------------------------------
1 | #missing source vm
2 | #source vm=
3 | source device=/dev/disk/by-uuid/8c3663c5-b9345-6381-9a67-dd813eb12863
4 | source mount point=/mnt-ex01
5 | source file=/containers/ex01-container.luks
6 | key=ex01-key
7 | destination vm 1 = d-testing
8 | destination inj 1 = /root/qcrypt-keys/ex01_disp
9 | destination mount point=/qcrypt-ex01
10 |
--------------------------------------------------------------------------------
/conf/test-invalid-02/ex01.ini:
--------------------------------------------------------------------------------
1 | ../examples/ex01.ini
--------------------------------------------------------------------------------
/conf/test-invalid-02/invalid02.ini:
--------------------------------------------------------------------------------
1 | #missing source file & key
2 | source vm=foo
3 | source device=/dev/disk/by-uuid/8c3663c5-b9345-6381-9a67-dd813eb12863
4 | source mount point=/mnt-ex01
5 | source file=
6 | key=
7 | destination vm 1 = d-testing
8 | destination inj 1 = /root/qcrypt-keys/ex01_disp
9 | destination mount point=/qcrypt-ex01
10 |
--------------------------------------------------------------------------------
/conf/test-invalid-03/invalid03.ini:
--------------------------------------------------------------------------------
1 | #missing destination vm
2 | source vm=foo
3 | source device=/dev/disk/by-uuid/8c3663c5-b9345-6381-9a67-dd813eb12863
4 | source mount point=/mnt-ex01
5 | source file=/tmp/foo
6 | key=fookey
7 | destination mount point=/qcrypt-ex01
8 |
--------------------------------------------------------------------------------
/conf/test-invalid-04/ex01.ini:
--------------------------------------------------------------------------------
1 | ../examples/ex01.ini
--------------------------------------------------------------------------------
/conf/test-invalid-04/invalid04.ini:
--------------------------------------------------------------------------------
1 | #invalid read-only value
2 | source vm=foo
3 | source file=/tmp/tmp.asd
4 | key=asdkey
5 | destination vm 1 = d-testing
6 | destination mount point=/qcrypt-ex01
7 | read-only=false!
8 |
--------------------------------------------------------------------------------
/conf/test-invalid-05/invalid05.ini:
--------------------------------------------------------------------------------
1 | #invalid read-only key
2 | source vm=foo
3 | source file=/tmp/tmp.asd
4 | key=asdkey
5 | destination vm 1 = d-testing
6 | destination mount point=/qcrypt-ex01
7 | readonly=false
8 |
--------------------------------------------------------------------------------
/conf/test-invalid-06/invalid06.ini:
--------------------------------------------------------------------------------
1 | #everything specified, typo (clommand)
2 | source vm=sys-usb
3 | source device=/dev/disk/by-uuid/8c3663c5-b9345-6381-9a67-dd813eb12863
4 | source mount point=/mnt-ex01
5 | source file=/containers/ex01-container.luks
6 | key=ex01-key
7 | destination vm 1 = d-testing
8 | destination inj 1 = /root/qcrypt-keys/ex01_disp
9 | destination vm 2 = work
10 | destination inj 2 = /another/path.key
11 | destination vm 3 = work2
12 | destination inj 3 = /another/path2.key
13 | destination mount point=/qcrypt-ex01
14 | autostart=true
15 | read-only=false
16 | pre open command=logger "starting the ex01 chain"
17 | post open command=logger "started the ex01 chain"
18 | pre close clommand=logger "attempting to close the ex01 chain"
19 | post close command=logger "stopped the ex01 chain"
20 |
--------------------------------------------------------------------------------
/conf/test-start-01/NOTE.txt:
--------------------------------------------------------------------------------
1 | This is a folder used by the qcryptd tests.
2 |
3 | The configuration files are updated during every test run.
4 |
--------------------------------------------------------------------------------
/conf/test-valid-01/ex01.ini:
--------------------------------------------------------------------------------
1 | #
2 | # A qcryptd configuration example for a single qcrypt chain. Each chain must be configured in a separate
3 | # configuration file.
4 | #
5 | # qcryptd will manage all chains which reside in the same 'target' folder specified during the qcryptd
6 | # start operation.
7 | #
8 |
9 | # VM where the encrypted container will be found once the source device is available (required).
10 | source vm=sys-usb
11 |
12 | # Block device on which the luks container resides (optional). It is recommended to specify it with a unique
13 | # identifier, e.g. by UUID. qcryptd will wait for this device and all necessary VMs to become available before
14 | # attempting to start the qcrypt chain.
15 | #
16 | # Not specifying any device will cause qcryptd to assume that the source file is always available in the root
17 | # file system (/).
18 | source device=/dev/disk/by-uuid/8c3663c5-b9345-6381-9a67-dd813eb12863
19 |
20 | # Mount point to use for the source device inside the source VM (optional). Not specifying a mount point will
21 | # cause qcryptd to assume that the source device is always available on your local file system (/).
22 | #
23 | # qcryptd will enforce that mount point regardless of whether the device is mounted elsewhere already.
24 | source mount point=/mnt-ex01
25 |
26 | # Path relative to the source mount point leading to the encrypted container file inside the source VM (required).
27 | source file=/containers/ex01-container.luks
28 |
29 | # Identifier of the key / file name of the key inside the ~/.qcrypt folder of all involved VMs (required)
30 | key=ex01-key
31 |
32 | # List of destination VMs to use for the chain (at least one is required) and key injections (optional).
33 | # The VM with the highest number is the one that is meant to read the plaintext.
34 | #
35 | # If you need disposable VMs, use named ones and inject the key! E.g.
36 | # `qvm-create --class DispVM --prop netvm='' --template nonet-dvm -l red d-testing`
37 | #
38 | # A single key injection may be specified per destination VM (default: none).
39 | #
40 | # WARNING: Injecting a key into the wrong VM will cause havoc to whatever security guarantees qcrypt provides.
41 | # Therefore make sure to not confuse the below indices!
42 | destination vm 1 = d-testing
43 | destination inj 1 = /root/qcrypt-keys/ex01_disp
44 |
45 | destination vm 2 = work
46 | #destination inj 2 =
47 |
48 | # Mount point of the plain text data in the destination VM (default: not mounted).
49 | destination mount point=/qcrypt-ex01
50 |
51 | # Autostart all VMs required for this chain (default: false).
52 | #autostart=false
53 |
54 | # Attach the chain in read-only mode (default: true).
55 | read-only=false
56 |
57 | # Command to run _before_ a chain open attempt (default: no command).
58 | # A non-zero exit code of this command will prevent the chain from being started.
59 | #pre open command=logger "starting the ex01 chain"
60 |
61 | # Command to run _after_ a successful chain open (default: no command).
62 | #post open command=logger "started the ex01 chain"
63 |
64 | # Command to run _before_ a chain close attempt (default: no command).
65 | # The exit code is ignored.
66 | #pre close command=logger "attempting to close the ex01 chain"
67 |
68 | # Command to run _after_ a successful chain close (default: no command).
69 | #post close command=logger "stopped the ex01 chain"
70 |
--------------------------------------------------------------------------------
/conf/test-valid-01/valid01.ini:
--------------------------------------------------------------------------------
1 | #minimal config
2 | source vm=foovm
3 | #source device=/dev/disk/by-uuid/8c3663c5-b9345-6381-9a67-dd813eb12863
4 | #source mount point=/mnt-ex01
5 | source file=/containers/ex01-container.luks
6 | key=ex01-key
7 | destination vm 1 = d-testing
8 | #destination inj 1 = /root/qcrypt-keys/ex01_disp
9 | #destination mount point=/qcrypt-ex01
10 |
--------------------------------------------------------------------------------
/conf/test-valid-02/ex01.ini:
--------------------------------------------------------------------------------
1 | #
2 | # A qcryptd configuration example for a single qcrypt chain. Each chain must be configured in a separate
3 | # configuration file.
4 | #
5 | # qcryptd will manage all chains which reside in the same 'target' folder specified during the qcryptd
6 | # start operation.
7 | #
8 |
9 | # VM where the encrypted container will be found once the source device is available (required).
10 | source vm=sys-usb
11 |
12 | # Block device on which the luks container resides (optional). It is recommended to specify it with a unique
13 | # identifier, e.g. by UUID. qcryptd will wait for this device and all necessary VMs to become available before
14 | # attempting to start the qcrypt chain.
15 | #
16 | # Not specifying any device will cause qcryptd to assume that the source file is always available in the root
17 | # file system (/).
18 | source device=/dev/disk/by-uuid/8c3663c5-b9345-6381-9a67-dd813eb12863
19 |
20 | # Mount point to use for the source device inside the source VM (optional). Not specifying a mount point will
21 | # cause qcryptd to assume that the source device is always available on your local file system (/).
22 | #
23 | # qcryptd will enforce that mount point regardless of whether the device is mounted elsewhere already.
24 | source mount point=/mnt-ex01
25 |
26 | # Path relative to the source mount point leading to the encrypted container file inside the source VM (required).
27 | source file=/containers/ex01-container.luks
28 |
29 | # Identifier of the key / file name of the key inside the ~/.qcrypt folder of all involved VMs (required)
30 | key=ex01-key
31 |
32 | # List of destination VMs to use for the chain (at least one is required) and key injections (optional).
33 | # The VM with the highest number is the one that is meant to read the plaintext.
34 | #
35 | # If you need disposable VMs, use named ones and inject the key! E.g.
36 | # `qvm-create --class DispVM --prop netvm='' --template nonet-dvm -l red d-testing`
37 | #
38 | # A single key injection may be specified per destination VM (default: none).
39 | #
40 | # WARNING: Injecting a key into the wrong VM will cause havoc to whatever security guarantees qcrypt provides.
41 | # Therefore make sure to not confuse the below indices!
42 | destination vm 1 = d-testing
43 | destination inj 1 = /root/qcrypt-keys/ex01_disp
44 |
45 | destination vm 2 = work
46 | #destination inj 2 =
47 |
48 | # Mount point of the plain text data in the destination VM (default: not mounted).
49 | destination mount point=/qcrypt-ex01
50 |
51 | # Autostart all VMs required for this chain (default: false).
52 | #autostart=false
53 |
54 | # Attach the chain in read-only mode (default: true).
55 | read-only=false
56 |
57 | # Command to run _before_ a chain open attempt (default: no command).
58 | # A non-zero exit code of this command will prevent the chain from being started.
59 | #pre open command=logger "starting the ex01 chain"
60 |
61 | # Command to run _after_ a successful chain open (default: no command).
62 | #post open command=logger "started the ex01 chain"
63 |
64 | # Command to run _before_ a chain close attempt (default: no command).
65 | # The exit code is ignored.
66 | #pre close command=logger "attempting to close the ex01 chain"
67 |
68 | # Command to run _after_ a successful chain close (default: no command).
69 | #post close command=logger "stopped the ex01 chain"
70 |
--------------------------------------------------------------------------------
/conf/test-valid-02/valid02.ini:
--------------------------------------------------------------------------------
1 | source vm=foovm
2 | source device=/dev/disk/by-uuid/8c3663c5-b9345-6381-9a67-dd813eb12863
3 | source mount point=/mnt-ex01
4 | source file=/containers/ex01-container.luks
5 | key=ex01-key
6 | destination vm 1 = d-testing
7 | destination inj 1 = /root/qcrypt-keys/ex01_disp
8 | destination mount point=/qcrypt-ex01
9 |
--------------------------------------------------------------------------------
/conf/test-valid-03/ex01.ini:
--------------------------------------------------------------------------------
1 | #
2 | # A qcryptd configuration example for a single qcrypt chain. Each chain must be configured in a separate
3 | # configuration file.
4 | #
5 | # qcryptd will manage all chains which reside in the same 'target' folder specified during the qcryptd
6 | # start operation.
7 | #
8 |
9 | # VM where the encrypted container will be found once the source device is available (required).
10 | source vm=sys-usb
11 |
12 | # Block device on which the luks container resides (optional). It is recommended to specify it with a unique
13 | # identifier, e.g. by UUID. qcryptd will wait for this device and all necessary VMs to become available before
14 | # attempting to start the qcrypt chain.
15 | #
16 | # Not specifying any device will cause qcryptd to assume that the source file is always available in the root
17 | # file system (/).
18 | source device=/dev/disk/by-uuid/8c3663c5-b9345-6381-9a67-dd813eb12863
19 |
20 | # Mount point to use for the source device inside the source VM (optional). Not specifying a mount point will
21 | # cause qcryptd to assume that the source device is always available on your local file system (/).
22 | #
23 | # qcryptd will enforce that mount point regardless of whether the device is mounted elsewhere already.
24 | source mount point=/mnt-ex01
25 |
26 | # Path relative to the source mount point leading to the encrypted container file inside the source VM (required).
27 | source file=/containers/ex01-container.luks
28 |
29 | # Identifier of the key / file name of the key inside the ~/.qcrypt folder of all involved VMs (required)
30 | key=ex01-key
31 |
32 | # List of destination VMs to use for the chain (at least one is required), key injections (optional) and additio-
33 | # nal cryptsetup options (optional).
34 | # The VM with the highest number is the one that is meant to read the plaintext.
35 | #
36 | # If you need disposable VMs, use named ones and inject the key! E.g.
37 | # `qvm-create --class DispVM --prop netvm='' --template nonet-dvm -l red d-testing`
38 | #
39 | # A single key injection may be specified per destination VM (default: none).
40 | #
41 | # WARNING: Injecting a key into the wrong VM will cause havoc to whatever security guarantees qcrypt provides.
42 | # Therefore make sure to not confuse the below indices!
43 | #
44 | # Note that intermediate VMs (all destination VMs apart from the last; i.e. destination vm 1 here) are auto-
45 | # matically started by qcrypt/qcryptd as needed. They are also shut down automatically during the chain close
46 | # process. So if you use more than a single encryption layer, please make sure to dedicate all of the intermediate
47 | # VMs to that qcrypt chain only. Otherwise the close may impact other chains or whatever else you're doing inside
48 | # that VM.
49 | #
50 | # The options allow to specify cryptsetup parameters for every single VM. They are only relevant for non-luks
51 | # containers as the header contains all relevant information for luks containers (default).
52 | destination vm 1 = d-testing
53 | destination inj 1 = /root/qcrypt-keys/ex01_disp
54 | destination opt 1 = --type plain --cipher aes-xts-plain64 -s 512 --hash sha512
55 |
56 | destination vm 2 = work
57 | #destination inj 2 =
58 |
59 | # Mount point of the plain text data in the destination VM (default: not mounted).
60 | destination mount point=/qcrypt-ex01
61 |
62 | # Autostart all VMs required for this chain (default: false).
63 | # By default, only intermediate VMs are automatically started (see above).
64 | #autostart=false
65 |
66 | # Attach the chain in read-only mode (default: true).
67 | read-only=false
68 |
69 | # The chain will not be attempted to be started more than every [startup interval] seconds (default: 300).
70 | # This is meant to prevent repeated startups during error situations. A qcryptd restart can be used to manually
71 | # try again.
72 | #startup interval=5
73 |
74 | # Command to run _before_ a chain open attempt (default: no command).
75 | # A non-zero exit code of this command will prevent the chain from being started.
76 | #pre open command=logger "starting the ex01 chain"
77 |
78 | # Command to run _after_ a successful chain open (default: no command).
79 | #post open command=logger "started the ex01 chain"
80 |
81 | # Command to run _before_ a chain close attempt (default: no command).
82 | # The exit code is ignored.
83 | #pre close command=logger "attempting to close the ex01 chain"
84 |
85 | # Command to run _after_ a successful chain close (default: no command).
86 | #post close command=logger "stopped the ex01 chain"
87 |
--------------------------------------------------------------------------------
/conf/test-valid-03/valid03.ini:
--------------------------------------------------------------------------------
1 | #almost everything specified
2 | source vm=another-usb
3 | source device=/dev/disk/by-uuid/8c3663c5-b9345-6381-9a67-dd813eb12864
4 | source mount point=/mnt-ex03
5 | source file=/containers/ex03-container.luks
6 | key=ex03-key
7 | destination vm 1 = d-testing
8 | destination inj 1 = /root/qcrypt-keys/ex03_disp
9 | destination opt 1 = --type luks
10 | destination vm 2 = work
11 | destination inj 2 = /another/path.key
12 | destination opt 2 =
13 | destination vm 3 = work2
14 | destination inj 3 = /another/path2.key
15 | destination opt 3 = --type luks
16 | destination mount point=/qcrypt-ex03
17 | autostart=true
18 | #read-only=false
19 | startup interval=5
20 | pre open command=logger "starting the ex03 chain"
21 | post open command=logger "started the ex03 chain"
22 | pre close command=logger "attempting to close the ex03 chain"
23 | post close command=logger "stopped the ex03 chain"
24 |
--------------------------------------------------------------------------------
/qcrypt:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #
3 | #See usage().
4 | #
5 | #Copyright (C) 2020 David Hobach GPLv3
6 | #version: 0.9
7 | #
8 | #This program is free software: you can redistribute it and/or modify
9 | #it under the terms of the GNU General Public License as published by
10 | #the Free Software Foundation, either version 3 of the License, or
11 | #(at your option) any later version.
12 | #
13 | #This program is distributed in the hope that it will be useful,
14 | #but WITHOUT ANY WARRANTY; without even the implied warranty of
15 | #MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 | #GNU General Public License for more details.
17 | #
18 | #You should have received a copy of the GNU General Public License
19 | #along with this program. If not, see .
20 | #
21 |
22 | #init blib
23 | source blib
24 | b_checkVersion 1 6 || { >&2 echo "This script depends on blib (https://github.com/3hhh/blib) version 1.6 or higher. Please install a supported version." ; exit 1 ; }
25 | eval "$B_SCRIPT"
26 | b_import "args"
27 | b_import "traps"
28 | b_import "fs"
29 | b_import "arr"
30 | b_import "keys"
31 | b_import "os/qubes4/dom0"
32 |
33 | #environment variable: user interaction mode (mostly for password prompts)
34 | #may be one of: auto|gui|tty
35 | #example: `export QCRYPT_UI_MODE="tty"`
36 | QCRYPT_UI_MODE="${QCRYPT_UI_MODE:-auto}"
37 |
38 | #distinguish the B_E exit code from the "normal" error
39 | B_RC=6
40 |
41 | #default options for b_dom0_qvmRun & b_dom0_exec*
42 | #shellcheck disable=SC2034
43 | B_DOM0_QVM_RUN_PARAMS=("--no-gui")
44 |
45 | #shellcheck disable=SC2088
46 | QCRYPT_FOLDER="~/.qcrypt"
47 | QCRYPT_KEYS="$QCRYPT_FOLDER/keys"
48 |
49 | #array of destination VMs, parsed by parseDestinations()
50 | declare -a DSTS
51 |
52 | #some global arrays required by statusAllC
53 | declare -a S_CAND
54 | declare -A S_DATA
55 | declare -A S_HOPS
56 | declare -A S_BLOCK
57 |
58 | function usage {
59 | echo "
60 | Usage: $B_SCRIPT_NAME [options] [command] [source vm] [source file] [key id] [destination vm 1] .. [destination vm n]
61 |
62 | Manage files encrypted with multiple encryption layers in Qubes OS dom0. Each layer is supposed to be decrypted inside a dedicated
63 | destination VM until the final destination/target VM can decrypt to the plaintext. The source VM doesn't decrypt itself.
64 |
65 | For most threat models it should suffice to have 1-2 layers of encryption. Each layer of course has a performance impact.
66 |
67 | - 1 layer can make sense if you don't trust your [source vm], but totally trust [destination vm 1]/the target VM to never become compromised.
68 | - 2 layers can make sense, if you don't trust [source vm], totally trust [destination vm 1] to only do its encrypting and decrypting job (it
69 | should be used exclusively for that) and only have mediocre trust in [destination vm 2]/the target VM. This way attacks starting from the
70 | target VM attempting to leak information by not doing its encryption job properly (e.g. by not encrypting at all) are prevented by
71 | [destination vm 1] doing its encryption job properly.
72 | - More layers can make sense if you want to trust the middle encryption/decryption VMs less and share your trust among them.
73 |
74 | [source vm] The VM where to find the data to decrypt ([source file]).
75 |
76 | [source file] Full path to the encrypted file inside the [source vm].
77 |
78 | [key id] An identifying string for the keys to use. Each VM must have its own key saved under that identifier.
79 | The identifier is the same for all VMs though. Keys are stored locally inside each VM at $QCRYPT_KEYS.
80 | The identifier may only consist of numbers and characters.
81 | If you lose the keys, you lose all encrypted data. So better create a backup.
82 |
83 | [destination vm 1 .. n] Layer of VMs to use for decryption in exactly that order. Each VM must have a matching decryption key.
84 | Recommendation: All destination VMs except of the final one should be dedicated to this particular qcrypt chain as
85 | qcrypt open/close may require to start or stop the VMs at will. Moreover it'll further increase the security level.
86 |
87 | [command] may be one of:
88 |
89 | open
90 | Map the [source file] in [source vm] to a device, attach it to [destination vm 1], then decrypt it there. Afterwards attach the result
91 | to [destination vm 2], decrypt there and so on.
92 |
93 | All VMs except for the source and final destination VM may be started automatically during the process.
94 |
95 | Options:
96 | -a Autostart all VMs required to be running for this operation (default: only autostart intermediate VMs).
97 | --mp [path] After all layers of encryptions are decrypted in [vm n], attempt to mount the decrypted data to [path].
98 | Otherwise the plaintext device is not mounted.
99 | --inj [vm] [key] Inject the given [key] (full path) from dom0 into the [vm] before opening. This can be useful for disposable VMs.
100 | The parameter can be specified multiple times. Won't override existing keys.
101 | If the [key] resides inside a dom0 blib key store, you can use keystore://[store directory]. To use the default key
102 | store, just use keystore:// .
103 | --cy [vm] [opt] Pass the options [opt] to cryptsetup when the encrypted device for the given [vm] is opened. This parameter may be
104 | specified multiple times. Multiple options can be concatenated; spaces in arguments may require escaping though. Should
105 | be used by experts only.
106 | --ro Attach all involved devices in read-only mode (default: r/w).
107 |
108 | status
109 | Check the online and decryption status of the [source vm], all intermediary VMs and the target VM. A non-zero status code indicates the
110 | number of missing steps towards decryption in the target VM.
111 |
112 | Invoking the status command without any other parameters will make $B_SCRIPT_NAME attempt to find all potential encryption chains.
113 |
114 | Options:
115 | --mp [path] Check whether the final device is mounted at the given [path]. If an empty [path] is specified, check whether it is
116 | mounted somewhere. Without this parameter, the exit code does not include the mount status.
117 |
118 | luksInit
119 | Create a new encrypted container in [source vm] as [source file] ([source file] must be a non-existing file path here) and pass the
120 | required keys to the intermediary VMs (path: $QCRYPT_KEYS) under the [key id]. Existing keys are never overwritten, but the algorithm
121 | will abort further processing. Only [destination vm n] will be able to access the plaintext.
122 |
123 | Currently only luks containers are supported.
124 |
125 | The initial container creation will happen in dom0.
126 |
127 | Options:
128 | -a Autostart all VMs required to be running for this operation (default: error out on stopped VMs).
129 | --size [size] Size to allocate for the container (default: 1G). Supported units: K, M, G, T.
130 | --wd [dir] Use the given directory in dom0 for all files temporarily created during the init process (default: /tmp/). Its capacity
131 | must be larger than the --size parameter. /tmp/ in Qubes OS usually only fits 2G.
132 | --ks [size] Size of the keys to deploy in bytes (default: 100).
133 | --bak [folder] Create an unencrypted backup of all keys in the given dom0 folder (default: no backup).
134 | --keystore [dir] Copy all keys to the encrypted blib key store found inside the dom0 directory [dir] (default: not used).
135 | Use '//' for the default key store at $(b_keys_getDefaultStore).
136 | --fs [type] Generate the given file system type with mkfs in the decrypted container (default: btrfs).
137 | --enkey [device] Block device to use as entropy source for the key generation (default: /dev/random).
138 | --encon [device] Block device to use as entropy source for the container initialization (default: /dev/urandom).
139 | --cy [vm] [opt] See open. The [vm] must be the one for which the container is created.
140 |
141 | close
142 | Detach the [source file] from [destination vm n] and all intermediary VMs. Exits with a zero status code if and only if all remnants
143 | on all VMs were detached.
144 |
145 | Options:
146 | --sd Shut down any VMs preventing a successful close operation. This can be useful if the close operation fails due to
147 | Qubes OS or libxenlight errors.
148 | --force Bypass any checks and attempt to close the given chain. This should be used if a previous detach only happened partially
149 | or you shut down one of the involved VMs without closing before.
150 | Important: This may shut down all involved destination VMs.
151 | help
152 | print this help"
153 | exit 1
154 | }
155 |
156 | #checkDependencies
157 | #Checks whether the current bash environment suffices the dependency requirements to run this script.
158 | #returns: Nothing, but errors out, if the dependencies are not met.
159 | #@B_E
160 | function checkDependencies {
161 | b_deps "head" "cryptsetup" "qvm-prefs" "qvm-block" "qvm-check" "losetup" \
162 | "df" "mktemp" "mkfs" "chmod" "readlink"
163 |
164 | local vmDeps="cryptsetup
165 | losetup
166 | findmnt
167 | head
168 | tar
169 | xargs"
170 | b_dom0_setVMDeps "$vmDeps"
171 | }
172 |
173 | #getSourceDeviceType [source file]
174 | #Function meant to run in the source VM in order to obtain the source device type.
175 | #[source file]: as passed to openC
176 | #returns: exit code of 0 = file & no loop device created, 7 = file, loop device exists, 8 = device, other = error
177 | function getSourceDeviceType {
178 | local sourceFile="$1"
179 | if [ -f "$sourceFile" ] ; then
180 | local out=""
181 | out="$(losetup -j "$sourceFile")" || exit 9
182 | [ -z "$out" ] && exit 0 || exit 7
183 | elif [ -b "$sourceFile" ] ; then
184 | exit 8
185 | else
186 | #error
187 | exit 9
188 | fi
189 | }
190 |
191 | #getKeyPath [vm] [key ID]
192 | #Obtain the key path from the given key id.
193 | #returns: key path and sets a non-zero exit code on errors
194 | #@B_E
195 | function getKeyPath {
196 | local vm="$1"
197 | local keyId="$2"
198 | local vmUser=""
199 | vmUser="$(qvm-prefs "$vm" default_user)" || { B_ERR="Failed to retrieve the default user for the VM $vm." ; B_E ; }
200 |
201 | echo "${QCRYPT_KEYS/#~/\/home\/$vmUser}/$keyId"
202 | }
203 |
204 | #parseAndCheckArgs "$@"
205 | #Parse all arguments of this script, apply some normalisations and do some preliminary option checks.
206 | #@B_E
207 | function parseAndCheckArgs {
208 | b_args_init 0 "--mp" 1 "--inj" 2 "--size" 1 "--wd" 1 "--ks" 1 "--bak" 1 "--fs" 1 "--enkey" 1 "--encon" 1 "--cy" 2 "--keystore" 1
209 | b_args_setOptionParamSeparator ""
210 | b_args_parse "$@"
211 |
212 | assertCorrectParams
213 | }
214 |
215 | #getCanonicalFileParameter [parameter index] [fallback]
216 | #Make the given parameter a canonical file path.
217 | #[parameter index]: index for b_args_get (default: 2)
218 | #returns: Sets a zero exit code only on successful canonicalisation.
219 | #shellcheck disable=SC2120
220 | function getCanonicalFileParameter {
221 | local ind="${1:-2}"
222 | local fb="$2"
223 | local ret=
224 |
225 | #canonicalize the file (users may pass e.g. /foo//bar --> /foo/bar however is expected by qcrypt)
226 | local file=
227 | file="$(b_args_get "$ind" "$fb")" || { ret=$? ; echo "$file" ; return $ret ; }
228 | if [ -n "$file" ] ; then
229 | readlink -m "$file" || { B_ERR="Failed to canonicalize the file: $file" ; B_E ; }
230 | fi
231 | return 0
232 | }
233 |
234 | #assertCorrectParams
235 | #Checks whether the command-line parameters are valid and if not, errors out.
236 | #@B_E
237 | function assertCorrectParams {
238 | local cmd="$(b_args_get 0)"
239 |
240 | local numArgs="$(b_args_getCount)"
241 | local numOpts="$(b_args_getOptionCount)"
242 |
243 | case "$cmd" in
244 | "open")
245 | b_args_assertOptions "-a" "--mp" "--inj" "--ro" "--cy"
246 | ;;
247 |
248 | "luksInit")
249 | #NOTE: we must also check for the supported luksFormat options here
250 | b_args_assertOptions "-a" "--size" "--wd" "--ks" "--bak" "--fs" "--enkey" "--encon" "--cy" "--keystore"
251 | ;;
252 |
253 | "close")
254 | b_args_assertOptions "--sd" "--force"
255 | ;;
256 |
257 | "status")
258 | [ $numArgs -eq 1 ] && [ $numOpts -eq 0 ] && return 0
259 | b_args_assertOptions "--mp"
260 | ;;
261 |
262 | *)
263 | usage
264 | ;;
265 | esac
266 |
267 | [ $numArgs -lt 5 ] && usage
268 |
269 | local keyId="$(b_args_get 3)"
270 | local keyRegex='^[0-9a-zA-Z_+.-]+$'
271 | [[ "$keyId" =~ $keyRegex ]] || { B_ERR="The given key ID $keyId appears to be invalid." ; B_E ; }
272 |
273 | #ensure all regular arguments are non-empty
274 | local i=
275 | for ((i=0;i<$numArgs;i++)) ; do
276 | [ -z "$(b_args_get $i)" ] && B_ERR="Found argument #$(($i +1)) to be empty." && B_E
277 | done
278 |
279 | return 0
280 | }
281 |
282 | #ensureClosed [source vm] [source file] [key id] [destination vm 1] ... [destination vm n]
283 | #Ensure that the given chain is fully closed (nothing attached anywhere, no open luks devices) and an open operation should succeed.
284 | #returns: Nothing, but calls [B_E](#B_E) on errors.
285 | #@B_E
286 | function ensureClosed {
287 | local state=""
288 | local ret=
289 | state="$(b_args_parse "status" "$@" ; statusSingleC)"
290 | ret=$?
291 |
292 | [[ "$state" == *"ERROR"* ]] && B_ERR="Failed to retrieve the chain state."$'\n'"$state" && B_E
293 | [ $ret -eq 0 ] && B_ERR="The chain is already open." && B_E
294 | local re='device mounted:[[:space:]]+no'
295 | [[ ! "$state" =~ $re ]] && B_ERR="The chain appears to be mounted. Overall state:"$'\n'"$state" && B_E
296 |
297 | #count "device attached: no" & "device decrypted: no"
298 | local reAttached='device attached:[[:space:]]+no'
299 | local reDecrypted='device decrypted:[[:space:]]+no'
300 | local devAttachedNoCnt=0
301 | local devDecryptedCnt=0
302 |
303 | while b_readLine ; do
304 | if [[ "$B_LINE" =~ $reAttached ]] ; then
305 | (( devAttachedNoCnt++ ))
306 | elif [[ "$B_LINE" =~ $reDecrypted ]] ; then
307 | (( devDecryptedCnt++ ))
308 | fi
309 | done <<< "$state"
310 |
311 | local numDest=$(( $# - 3 ))
312 | if [ $devAttachedNoCnt -eq $numDest ] && [ $devDecryptedCnt -eq $numDest ] ; then
313 | return 0
314 | else
315 | B_ERR="The chain is partially open. Please --force close it first. Overall state:"$'\n'"$state"
316 | B_E
317 | fi
318 | }
319 |
320 | #parseCryptsetupParams [map name] [destination VM 1] ... [destination VM n]
321 | #[map name]: Name of the map to return.
322 | #returns: A string which can be eval'ed to a map of VM --> additional cryptsetup options (all in one string, escaped) for that VM.
323 | #@B_E
324 | function parseCryptsetupParams {
325 | local mapName="$1"
326 | shift
327 |
328 | declare -A ret=()
329 | local i=0
330 | local vm=
331 | local par=
332 | while b_args_getOption "--cy" "" "$i" > /dev/null ; do
333 | vm="$(b_args_getOption "--cy" "" "$i" 0)" || { B_ERR="Failed to retrieve the VM for the cryptsetup option $i." ; B_E ; }
334 | par="$(b_args_getOption "--cy" "" "$i" 1)" || { B_ERR="Failed to retrieve the parameter for the cryptsetup option $i." ; B_E ; }
335 | b_arr_contains "$vm" "$@" || { B_ERR="The cryptsetup option VM $vm is not part of the destination VMs." ; B_E ; }
336 | #NOTE: we don't escape here as the user is allowed to concatenate parameters himself --> he needs to escape himself (but cryptsetup appears to have almost no options that might require escaping)
337 | ret["$vm"]="${ret["$vm"]} $par"
338 | i=$(($i +1))
339 | done
340 |
341 | ret="$(declare -p ret 2> /dev/null)"
342 | echo "${ret/declare -A ret/declare -A $mapName}"
343 | }
344 |
345 | #parseDestinations
346 | #Updates the [DSTS](#DSTS) array from the current command-line arguments (using the args module).
347 | #returns: Nothing.
348 | function parseDestinations {
349 | DSTS=()
350 | local i=
351 | local numArgs="$(b_args_getCount)"
352 | for ((i=4;i<$numArgs;i++)) ; do
353 | DSTS+=("$(b_args_get "$i")")
354 | done
355 | return 0
356 | }
357 |
358 | #initKeysModule [store dir]
359 | function initKeysModule {
360 | b_keys_init "$B_SCRIPT_NAME" 0 "$QCRYPT_UI_MODE" "" "" "$1"
361 | }
362 |
363 | #retrieveFromKeyStore run as root
364 | function retrieveFromKeyStore_root {
365 | local vm="$1"
366 | local keyId="$2"
367 | local storeDir="$3"
368 |
369 | initKeysModule "$storeDir"
370 | b_keys_get "${vm}_$keyId"
371 | }
372 |
373 | #retrieveFromKeyStore [vm] [key id] [store dir]
374 | #Retrieve the path to the given key in dom0 from the key store.
375 | #[vm]: VM for which to get the key.
376 | #[key id]: ID of the given key.
377 | #[store dir]: Directory of the key store to use (optional).
378 | #returns: Path to the key (may not exist) and sets a zero exit code on success.
379 | #@B_E
380 | function retrieveFromKeyStore {
381 | local vm="$1"
382 | local keyId="$2"
383 | local storeDir="$3"
384 | b_execFuncAs "root" "retrieveFromKeyStore_root" "fs" "multithreading/mtx" "dmcrypt" "keys" - - "$@" || { B_ERR="Failed to retrieve the key $keyId for the VM $vm from the key store $storeDir." ; B_E ; }
385 | }
386 |
387 | #see open @usage
388 | #@B_E
389 | function openC {
390 | local rwFlag=0
391 | local autostart=1
392 | declare -A injections
393 |
394 | #parse params
395 | local sourceVM="$(b_args_get 1)"
396 |
397 | local sourceFile=
398 | sourceFile="$(getCanonicalFileParameter)" || { B_ERR="Failed to canonicalize the source file parameter." ; B_E ; }
399 |
400 | local keyId="$(b_args_get 3)"
401 |
402 | parseDestinations
403 |
404 | b_args_getOption "-a" > /dev/null && autostart=0
405 |
406 | b_args_getOption "--ro" > /dev/null && rwFlag=1
407 |
408 | local mountPoint="$(b_args_getOption "--mp")"
409 |
410 | local i=0
411 | local injTarget=
412 | local injKey=
413 | while b_args_getOption "--inj" "" "$i" > /dev/null ; do
414 | injTarget="$(b_args_getOption "--inj" "" "$i" 0)" || { B_ERR="Failed to retrieve the target VM for the injection $i." ; B_E ; }
415 | injKey="$(b_args_getOption "--inj" "" "$i" 1)" || { B_ERR="Failed to retrieve the key for the injection $i." ; B_E ; }
416 | b_arr_contains "$injTarget" "${DSTS[@]}" || { B_ERR="The injection VM $injTarget is not part of the destination VMs." ; B_E ; }
417 | if [[ "$injKey" == "keystore://"* ]] ; then
418 | local storeDir="${injKey#keystore:/}"
419 | injKey="$(retrieveFromKeyStore "$injTarget" "$keyId" "$storeDir")" || { B_ERR="Could not resolve injection: Failed to retrieve the key $keyId for the VM $injTarget from the key store $storeDir." ; B_E ; }
420 | fi
421 | injections["$injTarget"]="$injKey"
422 | [ -f "$injKey" ] || { B_ERR="No such file: $injKey" ; B_E ; }
423 | i=$(($i +1))
424 | done
425 |
426 | local coptStr=
427 | coptStr="$(parseCryptsetupParams "copt" "${DSTS[@]}")" || { B_ERR="Failed to parse the cryptsetup parameters." ; B_E ; }
428 | eval "$coptStr" || { B_ERR="Programming error?!" ; B_E ; }
429 |
430 | #make sure that the open doesn't f*ck things up
431 | b_info "Checking whether the chain is fully closed..."
432 | b_setErrorHandler 'b_defaultErrorHandler 1 1 1'
433 | ensureClosed "$sourceVM" "$sourceFile" "$keyId" "${DSTS[@]}"
434 | b_resetErrorHandler 1
435 | if [[ "$B_ERR" == "The chain is already open." ]] ; then
436 | b_info "$B_ERR Nothing to do."
437 | B_ERR=""
438 | return 0
439 | fi
440 | B_E
441 |
442 | #start all necessary VMs or check that they are running
443 | if [ $autostart -eq 0 ] ; then
444 | b_info "Starting the VMs $sourceVM ${DSTS[*]}..." 0 1
445 | b_dom0_ensureRunning "$sourceVM" "${DSTS[@]}"
446 | b_info "Done." 1 0
447 | else
448 | #check source & dest and only autostart intermediate VMs
449 | b_dom0_isRunning "$sourceVM" "${DSTS[-1]}"
450 | declare -a interm=("${DSTS[@]::${#DSTS[@]}-1}")
451 | if [ ${#interm[@]} -gt 0 ] ; then
452 | b_info "Starting the intermediate VMs ${interm[*]}..." 0 1
453 | b_dom0_ensureRunning "${interm[@]}"
454 | b_info "Done." 1 0
455 | fi
456 | fi
457 |
458 | #create a source device if needed
459 | b_info "Preparing the source VM ${sourceVM}..."
460 | b_silence b_dom0_execFuncIn "$sourceVM" "" "getSourceDeviceType" - - "$sourceFile"
461 | case $? in
462 | 0)
463 | #all good
464 | ;;
465 | 7)
466 | #existing loop device
467 | b_dom0_removeUnusedLoopDevice "$sourceVM" "$sourceFile" 1 || { B_ERR="$sourceFile inside $sourceVM is either in use by Qubes OS or by the VM itself. Please check qvm-block ls. Backing off..." ; B_E ; }
468 |
469 | #all good
470 | ;;
471 | *)
472 | B_ERR="Failed to identify the source device type or the source is in an invalid state. Maybe the file doesn't exist?!"
473 | B_E
474 | esac
475 | #it is a file --> we need to create a loop device
476 | local toCreate="$sourceFile"
477 | sourceFile="$(b_dom0_createLoopDeviceIfNecessary "$sourceVM" "$toCreate")" || { B_ERR="Failed to create a loop device for the file $toCreate in the VM $sourceVM." ; B_E ; }
478 |
479 | #inject keys (if necessary)
480 | local injTarget=""
481 | local injKey=""
482 | local keyPath=""
483 | for injTarget in "${!injections[@]}" ; do
484 | injKey="${injections["$injTarget"]}"
485 | keyPath="$(getKeyPath "$injTarget" "$keyId")"
486 | b_info "Injecting the dom0 key $injKey into the VM $injTarget ($keyPath)..." 0 1
487 | local ret=2
488 | b_setBE 1
489 | #NOTE: we never overwrite!
490 | b_dom0_copy "$injKey" "$injTarget" "$keyPath" 1 1 2> /dev/null
491 | ret=$?
492 | b_resetErrorHandler 1
493 | if [ $ret -eq 0 ] ; then
494 | b_info "Done." 1 0
495 | else
496 | if [[ "$B_ERR" == *"blib_dom0_copyPrepareTarget failed"* ]] ; then
497 | #ignore errors caused due to existing files
498 | B_ERR=""
499 | b_info "Likely injected before." 1 0
500 | else
501 | b_info "Failed." 1 0
502 | B_E
503 | fi
504 | fi
505 | done
506 |
507 | #attach & decrypt
508 | local attachFrom="$sourceVM"
509 | local attachFromDevice="$sourceFile"
510 | local mpTo=""
511 | local mapperName="$keyId"
512 | local i=
513 | local lastInd=$(( ${#DSTS[@]} -1 ))
514 | for ((i=0;i<=$lastInd;i++)) ; do
515 | local attachTo="${DSTS[$i]}"
516 | local attachToDevice=""
517 |
518 | [ $i -eq $lastInd ] && mpTo="$mountPoint" || mpTo=""
519 |
520 | b_info "Attaching to ${attachTo}..."
521 | attachToDevice="$(b_dom0_crossAttachDevice "$attachFrom" "$attachFromDevice" "$attachTo" "$rwFlag")" || { B_ERR="Failed to attach the device $attachFromDevice from the VM $attachFrom to the VM $attachTo." ; B_E ; }
522 | keyPath="$(getKeyPath "$attachTo" "$keyId")" || { B_ERR="Failed to retrieve the key file path inside the VM $attachTo for the key ID $keyId." ; B_E ; }
523 | #NOTE: we use the key ID as device mapper name
524 | b_info "Decrypting inside ${attachTo}..."
525 | #shellcheck disable=SC2154
526 | b_dom0_openCrypt "$attachTo" "$attachToDevice" "$mapperName" "$rwFlag" "$mpTo" "$keyPath" "${copt["$attachTo"]}" || { B_ERR="Failed to decrypt the device $attachToDevice inside the VM $attachTo using the key file $keyPath." ; B_E ; }
527 |
528 | #special case: last run
529 | if [ $i -eq $lastInd ] ; then
530 | [ -n "$mpTo" ] && b_info "Mounted the decrypted data to: $mpTo"
531 | else
532 | #update vars
533 | #NOTE: unfortunately /dev/mapper/xyz is a symlink to /dev/dm-[0-9]+ and Qubes only accepts the latter to identify the backend --> we need to find that name from qvm-block
534 | #example for /dev/mapper/foo:
535 | #testing-vm:dm-2 foo
536 | local qvmBlockInfo=""
537 | qvmBlockInfo="$(b_dom0_parseQvmBlock "map")" || { B_ERR="Failed to parse qvm-block ls." ; B_E ; }
538 | attachFromDevice="$(b_dom0_getQvmBlockInfo "$qvmBlockInfo" "device id" "backend" "$attachTo" "description" "$mapperName" "frontend-dev" "")" || { B_ERR="Failed to find the correct backend device ID for the mapper $mapperName inside the VM $attachTo." ; B_E ; }
539 | attachFrom="$attachTo"
540 | fi
541 | done
542 |
543 | b_info "Open done."
544 | return 0
545 | }
546 |
547 | #ensureFileDoesNotExistIn [vm] [file]
548 | #Makes sure the given file doesn't exist in the given VM and errors out otherwise.
549 | #@B_E
550 | function ensureFileDoesNotExistIn {
551 | local vm="$1"
552 | local file="$2"
553 | local fileEsc=""
554 | printf -v fileEsc '%q' "$file"
555 |
556 | local cmd="[ -e $fileEsc ] && exit 5 || exit 0"
557 | local ret=-1
558 | b_silence b_dom0_qvmRun "$vm" "$cmd"
559 | ret=$?
560 | [ $ret -eq 5 ] && B_ERR="There already appears to exist a file named $file in the VM $vm. Will not overwrite." && B_E
561 | [ $ret -ne 0 ] && B_ERR="Failed to execute a command in the VM $vm." && B_E
562 | return 0
563 | }
564 |
565 | #checkAvailableSpace [directory] [required space (bytes)]
566 | #Check whether the given directory provides enough space.
567 | #returns: A zero exit code, if enough space is available and a non-zero exit code otherwise.
568 | #@B_E
569 | function checkAvailableSpace {
570 | local dir="$1"
571 | local req="$2"
572 | local avail=""
573 |
574 | avail="$(df -B1 --output=avail "$dir")" || { B_ERR="Failed to run df on the directory $dir." ; B_E ; }
575 |
576 | while b_readLine ; do
577 | [[ "$B_LINE" =~ ^([0-9]+) ]] && avail="${BASH_REMATCH[1]}" && break
578 | done <<< "$avail"
579 |
580 | [ $avail -gt $req ]
581 | }
582 |
583 | #see create @usage
584 | #@B_E
585 | function luksInitC {
586 | #parse params
587 | local sourceVM="$(b_args_get 1)"
588 | local sourceFile=
589 | sourceFile="$(getCanonicalFileParameter)" || { B_ERR="Failed to canonicalize the source file parameter." ; B_E ; }
590 | local keyId="$(b_args_get 3)"
591 | parseDestinations
592 |
593 | local autostart=1
594 | b_args_getOption "-a" > /dev/null && autostart=0
595 |
596 | local size="$(b_args_getOption "--size" "1073741824")"
597 | size="$(b_fs_parseSize "$size")" || { B_ERR="Failed to parse the --size parameter: $size" ; B_E ; }
598 |
599 | local workingDir="$(b_args_getOption "--wd" "/tmp")"
600 | mkdir -p "$workingDir" || { B_ERR="Failed to create $workingDir." ; B_E ; }
601 | [ -d "$workingDir" ] || { B_ERR="No directory: $workingDir" ; B_E ; }
602 |
603 | local keySize="$(b_args_getOptionInt "--ks" "100")"
604 |
605 | local enKey="$(b_args_getOption "--enkey" "/dev/random")"
606 | [ -c "$enKey" ] || { B_ERR="No valid entropy source: $enKey" ; B_E ; }
607 |
608 | local enCon="$(b_args_getOption "--encon" "/dev/urandom")"
609 | [ -c "$enCon" ] || { B_ERR="No valid entropy source: $enCon" ; B_E ; }
610 |
611 | local keyBackupFolder="$(b_args_getOption "--bak")"
612 | if [ -n "$keyBackupFolder" ] ; then
613 | mkdir -p "$keyBackupFolder" || { B_ERR="Failed to create $keyBackupFolder." ; B_E ; }
614 | [ -d "$keyBackupFolder" ] || { B_ERR="No directory: $keyBackupFolder" ; B_E ; }
615 | fi
616 |
617 | local keyStore="$(b_args_getOption "--keystore")"
618 | if [ -n "$keyStore" ] ; then
619 | [[ "$keyStore" == "//" ]] && keyStore="$(b_keys_getDefaultStore)"
620 | [ ! -d "$keyStore" ] && [ -e "$keyStore" ] && B_ERR="No directory: $keyStore" && B_E
621 | fi
622 |
623 | local fsType="$(b_args_getOption "--fs" "btrfs")"
624 |
625 | local coptStr=
626 | coptStr="$(parseCryptsetupParams "copt" "${DSTS[@]}")" || { B_ERR="Failed to parse the cryptsetup parameters." ; B_E ; }
627 | eval "$coptStr" || { B_ERR="Programming error?!" ; B_E ; }
628 |
629 | #luks2 has high memory requirements, which we need to fix (https://gitlab.com/cryptsetup/cryptsetup/-/issues/372)
630 | local dst=
631 | for dst in "${DSTS[@]}" ; do
632 | local opts="${copt["$dst"]}"
633 | if [[ "$opts" != *"--type"** ]] && [[ "$opts" != *"--pbkdf-memory"* ]] ; then
634 | #limit the memory requirements to 10MB
635 | copt["$dst"]="$opts --pbkdf-memory 10240"
636 | fi
637 | done
638 |
639 | #safety checks
640 | b_info "Doing some safety checks..."
641 | # a. we need to be root in dom0 & possibly some additional software
642 | b_enforceUser "root"
643 | [[ "$fsType" == "btrfs" ]] && b_deps "btrfs"
644 | # b. we need to have enough space in dom0
645 | checkAvailableSpace "$workingDir" "$size" || { B_ERR="$workingDir provides less space than required by the --size parameter." ; B_E ; }
646 | # c. all necessary VMs must run
647 | [ $autostart -eq 0 ] && b_dom0_ensureRunning "$sourceVM" "${DSTS[@]}" || b_dom0_isRunning "$sourceVM" "${DSTS[@]}"
648 | # d. make sure the source VM doesn't already have a file named $sourceFile
649 | ensureFileDoesNotExistIn "$sourceVM" "$sourceFile"
650 | # e. make sure the key ID is not used in any of the destination VMs
651 | local dst=""
652 | local vmKeyPath=""
653 | declare -A vmKeyPaths
654 | for dst in "${DSTS[@]}" ; do
655 | vmKeyPath="$(getKeyPath "$dst" "$keyId")" || { B_ERR="Failed to retrieve the key file path inside the VM $dst for the key ID $keyId." ; B_E ; }
656 | vmKeyPaths["$dst"]="$vmKeyPath"
657 | ensureFileDoesNotExistIn "$dst" "$vmKeyPath"
658 | done
659 |
660 | #init container
661 | local dom0Container=""
662 | local cmd=""
663 | dom0Container="$(mktemp -p "$workingDir")" || { B_ERR="Failed to create a temporary file." ; B_E ; }
664 | printf -v cmd 'rm -f %q' "$dom0Container"
665 | b_traps_add "$cmd" EXIT || { B_ERR="Failed to register a cleanup trap." ; B_E ; }
666 | b_info "Initializing the luks container (this may take a while)..."
667 | head -c $size < "$enCon" > "$dom0Container" || { B_ERR="Failed to initialize the container at $dom0Container." ; B_E ; }
668 |
669 | #create a loop device for the container
670 | local dom0ContainerLoop=""
671 | dom0ContainerLoop="$(losetup -f --show "$dom0Container")" || { B_ERR="Failed to create a loop device." ; B_E ; }
672 | printf -v cmd 'losetup -d %q' "$dom0ContainerLoop"
673 | b_traps_prepend "$cmd" EXIT || { B_ERR="Failed to register a cleanup trap." ; B_E ; }
674 |
675 | #generate key files & add encryption layers
676 | local dom0KeyFile=""
677 | declare -A dom0KeyFiles
678 | local curDevice="$dom0ContainerLoop"
679 | for dst in "${DSTS[@]}" ; do
680 | #generate key
681 | b_info "Generating the key for the VM ${dst}..."
682 | dom0KeyFile="$(mktemp -p "$workingDir")" || { B_ERR="Failed to create a temporary file." ; B_E ; }
683 | printf -v cmd 'b_fs_removeRelativelySafely %q' "$dom0KeyFile"
684 | b_traps_add "$cmd" EXIT || { B_ERR="Failed to register a cleanup trap." ; B_E ; }
685 | head -c "$keySize" < "$enKey" > "$dom0KeyFile" || { B_ERR="Failed to initialize the key for the VM $dst at $dom0KeyFile." ; B_E ; }
686 | dom0KeyFiles["$dst"]="$dom0KeyFile"
687 |
688 | #format
689 | b_info "Generating the encryption layer for the VM ${dst}..."
690 | cryptsetup -q --key-file "$dom0KeyFile" ${copt["$dst"]} luksFormat "$curDevice" || { B_ERR="Failed to run cryptsetup luksFormat." ; B_E ; }
691 |
692 | #open the newly created layer
693 | b_info "Switching to the next encryption layer..."
694 | local luksName="$dst-$keyId"
695 | cryptsetup open --type luks --key-file "$dom0KeyFile" "$curDevice" "$luksName" || { B_ERR="Failed to open the encryption layer created for the VM $dst." ; B_E ; }
696 | printf -v cmd 'cryptsetup close %q' "$luksName"
697 | b_traps_prepend "$cmd" EXIT || { B_ERR="Failed to register a cleanup trap." ; B_E ; }
698 | curDevice="/dev/mapper/$luksName"
699 | done
700 |
701 | #optional: generate file system
702 | if [ -n "$fsType" ] ; then
703 | b_info "Creating the file system..."
704 | mkfs -t "$fsType" "$curDevice" &> /dev/null || { B_ERR="Failed to create the file system of type $fsType on $curDevice." ; B_E ; }
705 |
706 | if [[ "$fsType" == "btrfs" ]] ; then
707 | b_info "Enabling btrfs zstd compression..."
708 | #unfortunately we have to mount it for that
709 | local tmpMp=
710 | tmpMp="$(mktemp -d)" || { B_ERR="Failed to create a temporary directory." ; B_E ; }
711 | mount "$curDevice" "$tmpMp" || { B_ERR="Failed to mount the btrfs device $curDevice to $tmpMp." ; B_E ; }
712 | btrfs property set "$tmpMp" compression zstd || { B_ERR="Failed to enable btrfs compression." ; B_E ; }
713 | umount "$tmpMp" || { B_ERR="Failed to umount the btrfs mount $tmpMp." ; B_E ; }
714 | rm -rf "$tmpMp" || { B_ERR="Failed to remove $tmpMp." ; B_E ; }
715 | fi
716 | fi
717 |
718 | #optional: create a backup of the keys in dom0
719 | #NOTE: if this fails, we don't need to pass all the stuff to the VMs
720 | if [ -n "$keyBackupFolder" ] ; then
721 | b_info "Backing up all keys to $keyBackupFolder..."
722 |
723 | for dst in "${DSTS[@]}" ; do
724 | dom0KeyFile="${dom0KeyFiles["$dst"]}"
725 | local bakFile="$keyBackupFolder/${dst}_$keyId"
726 | [ -e "$bakFile" ] && B_ERR="A file called $bakFile already exists. Rejecting to overwrite." && B_E
727 | cp "$dom0KeyFile" "$bakFile" || { B_ERR="Failed to copy the key file $dom0KeyFile to $bakFile." ; B_E ; }
728 | chmod 666 "$bakFile"
729 | done
730 | fi
731 |
732 | #optional: copy the keys to the key store
733 | if [ -n "$keyStore" ] ; then
734 | b_info "Adding the keys to the blib key store $keyStore..."
735 | initKeysModule "$keyStore"
736 |
737 | for dst in "${DSTS[@]}" ; do
738 | dom0KeyFile="${dom0KeyFiles["$dst"]}"
739 | b_keys_add "${dst}_$keyId" "$dom0KeyFile"
740 | done
741 | fi
742 |
743 | #pass the keys to the VMs
744 | for dst in "${DSTS[@]}" ; do
745 | dom0KeyFile="${dom0KeyFiles["$dst"]}"
746 | vmKeyPath="${vmKeyPaths["$dst"]}"
747 | b_info "Passing the key $dom0KeyFile to the VM $dst as $vmKeyPath..."
748 | b_dom0_copy "$dom0KeyFile" "$dst" "$vmKeyPath" 1 1 || { B_ERR="Failed to copy the key file $dom0KeyFile." ; B_E ; }
749 | done
750 |
751 | #pass the container to the source VM
752 | b_info "Copying the encrypted container to the VM $sourceVM at ${sourceFile}..."
753 | sync
754 | b_dom0_copy "$dom0Container" "$sourceVM" "$sourceFile" 1 1 || { B_ERR="Failed to copy the encrypted container to the source VM $sourceVM." ; B_E ; }
755 | sync
756 |
757 | b_info "Luks init done."
758 | b_info "Cleaning up... (this is not hanging)"
759 | return 0
760 | }
761 |
762 | #closeDecryptedData [mapper name]
763 | #Closes the given luks container inside a target VM.
764 | #[mapper name]: Name of the device mapper to close.
765 | #returns: 11, if the device mapper did not exist (wasn't open), 22 on cryptsetup errors during closing
766 | function closeDecryptedData {
767 | local mapperName="$1"
768 | local mapperNameEsc=""
769 | printf -v mapperNameEsc '%q' "$mapperName"
770 |
771 | ! [ -b /dev/mapper/$mapperNameEsc ] && exit 11
772 |
773 | cryptsetup close $mapperNameEsc || exit 22
774 | exit 0
775 | }
776 |
777 | #closeSourceLoop [source vm] [source file]
778 | #@B_E
779 | function closeSourceLoop {
780 | local sourceVM="$1"
781 | local sourceFile="$2"
782 | local ret=
783 |
784 | b_info "Closing the loop device inside $sourceVM..." 0 1
785 | b_setBE 1
786 | b_dom0_execFuncIn "$sourceVM" "" "detachLoop" - - "$sourceFile" &> /dev/null
787 | ret=$?
788 | b_resetErrorHandler
789 | if [ $ret -eq 17 ] ; then
790 | b_info "No loop device found. All good." 1 0
791 | elif [ $ret -eq 0 ] ; then
792 | b_info "Done." 1 0
793 | else
794 | #last action, no need for force
795 | B_ERR="Failed to close."
796 | B_E
797 | fi
798 | return 0
799 | }
800 |
801 | #cleanClose [force flag] [shutdown flag] [source VM] [source file] [key id] [destination 1] ... [destination n]
802 | #Attempt to perform a "clean" close without shutting down any VMs.
803 | #returns: Exits with a zero status code if and only if all remnants on all VMs were detached.
804 | #@B_E
805 | function cleanClose {
806 | local force="$1" #currently cannot be 0
807 | local shutdown="$2"
808 | local sourceVM="$3"
809 | local sourceFile="$4"
810 | local keyId="$5"
811 | shift 5
812 | declare -a dsts=("$@")
813 |
814 | #NOTE: we use the key ID as luks mapper name
815 | local mapperName="$keyId"
816 | local mapperNameEsc=""
817 | printf -v mapperNameEsc '%q' "$mapperName"
818 |
819 | #attempt umount in the final destination VM
820 | local err=""
821 | local dst="${dsts[*]: -1}"
822 | local ret=-1
823 | b_info "Umounting the plaintext device inside ${dst}..." 0 1
824 | local cmd="findmnt -n -o TARGET -S /dev/mapper/$mapperNameEsc | head -n1 | xargs umount -A -R || exit 17"
825 | b_setBE 1
826 | b_dom0_qvmRun "$dst" "$cmd" &> /dev/null
827 | ret=$?
828 | b_resetErrorHandler
829 | if [ $ret -eq 17 ] ; then
830 | b_info "Was not mounted. All good." 1 0
831 | elif [ $ret -eq 0 ] ; then
832 | b_info "Done." 1 0
833 | else
834 | err="Failed to umount."
835 | [ $force -eq 0 ] && b_info "$err Proceeding anyway..." 1 0 || { B_ERR="$err No point in proceeding." ; B_E ; }
836 | fi
837 |
838 | #cryptsetup close & detach afterwards in all VMs (if necessary)
839 | local qvmBlockInfo=""
840 | qvmBlockInfo="$(b_dom0_parseQvmBlock "map")" || { B_ERR="Failed to parse qvm-block ls." ; B_E ; }
841 | local i=
842 | local desc=
843 | for ((i=${#dsts[@]}-1;i >= 0;i--)) ; do
844 | local vm="${dsts[$i]}"
845 |
846 | #close
847 | b_info "Closing the dm-crypt device inside ${vm}..." 0 1
848 | b_setBE 1
849 | b_dom0_execFuncIn "$vm" "" "closeDecryptedData" - - "$mapperName" &> /dev/null
850 | ret=$?
851 | b_resetErrorHandler
852 | case $ret in
853 | 0)
854 | b_info "Done." 1 0
855 | ;;
856 | 11)
857 | b_info "Was not open. All good." 1 0
858 | ;;
859 | *)
860 | err="Failed to close the dm-crypt device (status: $ret)."
861 | [ $force -eq 0 ] && b_info "$err Proceeding anyway..." 1 0 || { B_ERR="$err No point in proceeding." ; B_E ; }
862 | esac
863 |
864 | #detach
865 | #example qvm-block ls output:
866 | #result of `qcrypt open -- disp287 /tmp/foo4 foo4 testing-pers d-testing`
867 | #BACKEND:DEVID DESCRIPTION USED BY
868 | #disp287:loop0 /tmp/foo4 testing-pers (read-only=no, frontend-dev=xvdi)
869 | #testing-pers:dm-0 foo4 d-testing (read-only=no, frontend-dev=xvdi)
870 | #foo4 is the /dev/mapper/foo4 name inside testing-pers providing a symlink to /dev/dm-0 and used by d-testing as /dev/xvdi
871 | #--> we'd have to detach:
872 | # qvm-block d d-testing testing-pers:dm-0
873 | # qvm-block d testing-pers disp287:loop0
874 | #--> resulting in an empty "used by" column
875 | [ $i -eq 0 ] && desc="$sourceFile" || desc="$mapperName"
876 | b_info "Detaching the device from the VM $vm..." 0 1
877 | local backend=""
878 | backend="$(b_dom0_getQvmBlockInfo "$qvmBlockInfo" "id" "description" "$desc" "used by" "$vm")"
879 | [ $? -eq $B_RC ] && { B_ERR="Failed to parse qvm-block ls." ; B_E ; }
880 | if [ -z "$backend" ] ; then
881 | b_info "Was not attached. All good." 1 0
882 | else
883 | qvm-block d "$vm" "$backend" &> /dev/null
884 | if [ $? -eq 0 ] ; then
885 | b_info "Done." 1 0
886 | else
887 | if [ $shutdown -eq 0 ] ; then
888 | #similar to what --force does, but we attempted to properly detach it before
889 | b_info "Failed, trying to shutdown... " 1 1
890 | b_dom0_ensureHalted "$vm"
891 | b_info "Done." 1 0
892 | else
893 | err="Failed to detach the backend $backend from the VM $vm. No point in proceeding."
894 | [ $force -eq 0 ] && b_info "$err Proceeding anyway..." 1 0 || { B_ERR="$err No point in proceeding." ; B_E ; }
895 | fi
896 | fi
897 | fi
898 | done
899 |
900 | closeSourceLoop "$sourceVM" "$sourceFile"
901 |
902 | [ -z "$err" ]
903 | }
904 |
905 | #dirtyClose [source VM] [source file] [key id] [destination 1] ... [destination n]
906 | #Attempt to perform a "dirty" close by simply shutting down all destination VMs.
907 | #returns: Exits with a zero status code if and only if all remnants on all VMs were detached.
908 | #@B_E
909 | function dirtyClose {
910 | local sourceVM="$1"
911 | local sourceFile="$2"
912 | local keyId="$3"
913 | shift 3
914 | declare -a dsts=("$@")
915 |
916 | local i=
917 | for ((i=${#dsts[@]}-1;i >= 0;i--)) ; do
918 | local vm="${dsts[$i]}"
919 | #NOTES:
920 | # - using qvm-block in any way is likely to trigger Qubes OS bug #4784
921 | # - we better shut down one after another as this will give Qubes OS some time to do the device detach cleanup (and in order)
922 | b_info "Shutting down the VM ${vm}..." 0 1
923 | b_dom0_ensureHalted "$vm"
924 |
925 | #give Qubes OS some time to clean up the mess (i.e. do the detach)
926 | sleep 0.2
927 | b_info "Done." 1 0
928 | done
929 |
930 | closeSourceLoop "$sourceVM" "$sourceFile"
931 |
932 | return 0
933 | }
934 |
935 | #see close @usage
936 | #@B_E
937 | function closeC {
938 | local force=1
939 | local shutdown=1
940 |
941 | #parse params
942 | b_args_getOption "--force" > /dev/null && force=0
943 | b_args_getOption "--sd" > /dev/null && shutdown=0
944 |
945 | local sourceVM="$(b_args_get 1)"
946 | local sourceFile=
947 | sourceFile="$(getCanonicalFileParameter)" || { B_ERR="Failed to canonicalize the source file parameter." ; B_E ; }
948 | local keyId="$(b_args_get 3)"
949 | parseDestinations
950 |
951 | #make sure the given chain is valid by checking its status
952 | #NOTE: we also check that the chain is mounted as it might otherwise be incomplete (missing target VM) and we don't want a partial closure
953 | if [ $force -ne 0 ] ; then
954 | b_info "Checking the validity of the chain..." 0 1
955 | local state=""
956 | state="$(b_args_parse "status" "$sourceVM" "$sourceFile" "$keyId" "${DSTS[@]}" ; statusSingleC)" || { B_ERR="The chain has a bad status - check it below. If you want to continue nonetheless, use --force."$'\n'"$state" ; B_E ; }
957 | b_info "Done. All good." 1 0
958 | cleanClose "$force" "$shutdown" "$sourceVM" "$sourceFile" "$keyId" "${DSTS[@]}"
959 | else
960 | b_info "Force mode. Thus attempting a dirty close..."
961 | # Reasoning: Qubes OS bug #4784 (https://github.com/QubesOS/qubes-issues/issues/4784) will make the system unusable until the next reboot, if any of the destination VMs are shut down and the above "clean" close is attempted afterwards.
962 | # Available workarounds:
963 | # 1. Never close and re-use the leftover open devices from previous qcrypt open operations. <-- not chosen
964 | # 2. Perform the below "dirty" close by shutting down all destination VMs _without_ using qvm-block at all. This prevents bug #4784 from being triggered as of Qubes OS 4.0.1. <-- chosen
965 | dirtyClose "$sourceVM" "$sourceFile" "$keyId" "${DSTS[@]}"
966 | fi
967 |
968 | b_info "Close done."
969 | return $ret
970 | }
971 |
972 | #detachLoop [file]
973 | #Detach the loop device associated with the given file, if it exists.
974 | #returns: an exit code of 17, if it doesn't exist; an exit code of 0 on success and a non-zero exit code on failure
975 | function detachLoop {
976 | local filePath="$1"
977 | local found="$(losetup -n -O NAME -j "$filePath")"
978 | if [ -z "$found" ] ; then
979 | exit 17
980 | else
981 | losetup -d "$found"
982 | fi
983 | }
984 |
985 | #getVMStatus [key path] [mapper name] [device name] [mount path]
986 | #Get the encryption state for the VM this function is running in.
987 | #[device name]: Name of the _encrypted_ attached device; may be empty, if no device is attached.
988 | #returns: 0 = key is available & device decrypted & mounted at the mount path (or somewhere, if not specified), 1 = key is available & device decrypted & not mounted, 2 = key available & device not decrypted, 3 = key unavailable & device not decrypted, 9 = other error
989 | function getVMStatus {
990 | local keyPath="$1"
991 | local mapperName="$2"
992 | local mapperPath="/dev/mapper/$mapperName"
993 | local devName="$3"
994 | local mp="$4"
995 | local mpList=""
996 |
997 | local keyAvailable=-1
998 | [ -f "$keyPath" ] && keyAvailable=0 || keyAvailable=1
999 |
1000 | #special case: no device
1001 | [ -z "$devName" ] && return $(( $keyAvailable +2 ))
1002 |
1003 | local cryptAvailable=-1
1004 | cryptsetup status "$mapperName" &> /dev/null && cryptAvailable=0 || cryptAvailable=1
1005 |
1006 | if [ $keyAvailable -eq 0 ] ; then
1007 | if [ $cryptAvailable -eq 0 ] ; then
1008 |
1009 | mpList="$(findmnt -l -o TARGET -n -S "$mapperPath")" || return 1
1010 | if [ -z "$mp" ] ; then
1011 | return 0
1012 | else
1013 | b_listContains "$mpList" "$mp" && return 0 || return 1
1014 | fi
1015 | else
1016 | return 2
1017 | fi
1018 | else
1019 | if [ $cryptAvailable -eq 0 ] ; then
1020 | return 9
1021 | else
1022 | return 3
1023 | fi
1024 | fi
1025 | }
1026 |
1027 | #see status @usage
1028 | #@B_E
1029 | function statusSingleC {
1030 | #parse params
1031 | local mountPoint=
1032 | local mpSpecified=
1033 | mountPoint="$(b_args_getOption "--mp")"
1034 | mpSpecified=$?
1035 |
1036 | local sourceVM="$(b_args_get 1)"
1037 | local sourceFile=
1038 | sourceFile="$(getCanonicalFileParameter)" || { B_ERR="Failed to canonicalize the source file parameter." ; B_E ; }
1039 | local keyId="$(b_args_get 3)"
1040 | parseDestinations
1041 |
1042 | #target format:
1043 | #sourceVM
1044 | # running: y/n
1045 | # source: block device|file, loop device|file, no loop device created|unknown, [reason]
1046 | #dst vm x
1047 | # running: y/n
1048 | # dev attached: y/n (device path) (from qvm-block ls)
1049 | # key available: y/n (file path) (file exists)
1050 | # dev decrypted: y/n (cryptsetup status)
1051 | #final vm
1052 | # [same as dst vm x and the below:]
1053 | # dev mounted: y/n (findmnt)
1054 |
1055 | echo ""
1056 | local stat=""
1057 | local runningStat=""
1058 | local ret=0
1059 | local statusFormat="%20s: %-10s\n"
1060 |
1061 | #source VM status
1062 | #NOTE: we use qvm-check over b_dom0_isRunning as the first doesn't error out and is less expensive (it accepts paused VMs though)
1063 | echo " $sourceVM"
1064 | if qvm-check --running "$sourceVM" &> /dev/null ; then
1065 | runningStat="yes"
1066 |
1067 | b_silence b_dom0_execFuncIn "$sourceVM" "" "getSourceDeviceType" - - "$sourceFile"
1068 | case $? in
1069 | 0)
1070 | stat="file, no loop device created"
1071 | ((++ret))
1072 | ;;
1073 | 7)
1074 | stat="file, loop device"
1075 | ;;
1076 | 8)
1077 | #shouldn't happen
1078 | stat="block device"
1079 | ;;
1080 | *)
1081 | stat="unknown, not available?"
1082 | ret=$(( $ret +2 ))
1083 | esac
1084 | else
1085 | runningStat="no"
1086 | stat="unknown, VM down"
1087 | ret=$(( $ret +3 ))
1088 | fi
1089 | printf "$statusFormat" "running" "$runningStat"
1090 | printf "$statusFormat" "source" "$stat"
1091 |
1092 | #destination VMs
1093 | local dst=
1094 | local keyPath=""
1095 | local devName=""
1096 | local lastVM="$sourceVM"
1097 | local lastDescription="$sourceFile"
1098 | local qvmBlockInfo=""
1099 | local lastUsedBy=""
1100 | local target="${DSTS[*]: -1}"
1101 | qvmBlockInfo="$(b_dom0_parseQvmBlock "map")" || { B_ERR="Failed to parse qvm-block ls." ; B_E ; }
1102 | for dst in "${DSTS[@]}" ; do
1103 | echo ""
1104 |
1105 | #running?
1106 | echo " $dst"
1107 | if qvm-check --running "$dst" &> /dev/null ; then
1108 | printf "$statusFormat" "running" "yes"
1109 | else
1110 | printf "$statusFormat" "running" "no"
1111 | printf "$statusFormat" "device attached" "no (not running)"
1112 | printf "$statusFormat" "key available" "no (not running)"
1113 | printf "$statusFormat" "device decrypted" "no (not running)"
1114 | statMounted="no (not running)"
1115 | ret=$(( $ret +4 ))
1116 | continue
1117 | fi
1118 |
1119 | #some needed parameters
1120 | keyPath="$(getKeyPath "$dst" "$keyId")" || { B_ERR="Failed to retrieve the key path for the VM $dst." ; B_E ; }
1121 | #example qvm-block ls output:
1122 | #result of `qcrypt open -- disp287 /tmp/foo4 foo4 testing-pers d-testing`
1123 | #BACKEND:DEVID DESCRIPTION USED BY
1124 | #disp287:loop0 /tmp/foo4 testing-pers (read-only=no, frontend-dev=xvdi)
1125 | #testing-pers:dm-0 foo4 d-testing (read-only=no, frontend-dev=xvdi)
1126 | #d-testing:dm-0 foo4 <--- line sometimes missing!
1127 | #foo4 is the /dev/mapper/foo4 name inside testing-pers providing a symlink to /dev/dm-0 and used by d-testing as /dev/xvdi
1128 | #NOTE @devName:
1129 | # - the description in Qubes OS is the name of the device mapper (and the device for the sourceVM) --> we know that the mapper is the keyId by convention, i.e. we can search for that
1130 | # - we need to find the frontend-dev with a backend equal to the last VM
1131 | devName="$(b_dom0_getQvmBlockInfo "$qvmBlockInfo" "frontend-dev" "backend" "$lastVM" "description" "$lastDescription")"
1132 | [ $? -eq $B_RC ] && { B_ERR="Failed to parse qvm-block ls." ; B_E ; }
1133 |
1134 | #make sure the chain makes sense / ignore non-matching devices
1135 | lastUsedBy="$(b_dom0_getQvmBlockInfo "$qvmBlockInfo" "used by" "backend" "$lastVM" "description" "$lastDescription")"
1136 | [ $? -eq $B_RC ] && { B_ERR="Failed to parse qvm-block ls." ; B_E ; }
1137 | [[ "$dst" != "$lastUsedBy" ]] && devName=""
1138 |
1139 | #device attached?
1140 | [ -n "$devName" ] && stat="yes" || { stat="no" ; ((++ret)) ; }
1141 | printf "$statusFormat" "device attached" "$stat"
1142 |
1143 | #key available, device decrypted, mounted?
1144 | local tRet=
1145 | b_silence b_dom0_execFuncIn "$dst" "" "getVMStatus" - - "$keyPath" "$keyId" "$devName" "$mountPoint"
1146 | tRet=$?
1147 | local statDev=""
1148 | local statMounted="no"
1149 | case $tRet in
1150 | 0)
1151 | stat="yes"
1152 | statDev="yes"
1153 | statMounted="yes"
1154 | ;;
1155 | 1)
1156 | stat="yes"
1157 | statDev="yes"
1158 | ;;
1159 | 2)
1160 | stat="yes"
1161 | statDev="no"
1162 | ((++ret))
1163 | ;;
1164 | 3)
1165 | stat="no"
1166 | statDev="no"
1167 | ret=$(( $ret +2 ))
1168 | ;;
1169 | *)
1170 | B_ERR="The VM $dst returned a strange status for itself. Maybe it is not managed by qcrypt? You might have to investigate."
1171 | B_E
1172 | esac
1173 | printf "$statusFormat" "key available" "$stat"
1174 | printf "$statusFormat" "device decrypted" "$statDev"
1175 |
1176 | #update for next iteration
1177 | lastVM="$dst"
1178 | lastDescription="$keyId"
1179 | done
1180 |
1181 | #dev mounted on final VM?
1182 | [ $mpSpecified -eq 0 ] && [[ "$statMounted" != "yes" ]] && ((++ret))
1183 | printf "$statusFormat" "device mounted" "$statMounted"
1184 |
1185 | #make sure the target is not used by anything (otherwise it would be an invalid target)
1186 | #NOTE: if nothing is attached/mounted/whatever, this check will also succeed
1187 | lastUsedBy="$(b_dom0_getQvmBlockInfo "$qvmBlockInfo" "used by" "backend" "$target" "description" "$keyId")"
1188 | [ $? -eq $B_RC ] && { B_ERR="Failed to parse qvm-block ls." ; B_E ; }
1189 | [ -n "$lastUsedBy" ] && B_ERR="The target VM $target appears to be used by $lastUsedBy as part of a chain. Is the chain incomplete?" && B_E
1190 |
1191 | #set exit code
1192 | exit $ret
1193 | }
1194 |
1195 | #statusAllFindNextHop [candidate id] [expected description]
1196 | #Find the next "used by" hop matching the criteria of the GEN_PATHS algorithm in step 1b. Save its ID under [candidate ID]_next[DESCRIPTION of next hop] inside S_DATA.
1197 | #[candidate id]: BACKEND:DEVID of the candidate
1198 | #[expected description]: description to check against; if none is specified, all are fine
1199 | #returns: 0, if a valid candidate chain was found and a non-zero exit code otherwise
1200 | function statusAllFindNextHop {
1201 | local cand="$1"
1202 | [ -z "$cand" ] && return 1
1203 | local expectedDesc="$2"
1204 | local candUsedBy="${S_DATA["${cand}_used by"]}"
1205 | local found=0
1206 | local i=
1207 |
1208 | #find all next hop candidates
1209 | #maybe TODO: improve performance by replacing the loop search with a data structure (back reference or sth similar)
1210 | for ((i=0; i < ${S_BLOCK["max"]}; i++)) ; do
1211 | local cur="${S_BLOCK["${i}_id"]}"
1212 | local curBackend="${S_BLOCK["${i}_backend"]}"
1213 | local curDesc="${S_BLOCK["${i}_description"]}"
1214 |
1215 | if [ -n "$curDesc" ] && [[ "$curBackend" == "$candUsedBy" ]] && [[ "${S_BLOCK["${i}_device id"]}" =~ ^dm\-[0-9]+$ ]] ; then
1216 | [ -n "$expectedDesc" ] && [[ "$curDesc" != "$expectedDesc" ]] && continue
1217 |
1218 | #next hop found
1219 | #NOTE: we may find multiple for the first hop!
1220 | found=$(( $found +1 ))
1221 | S_HOPS["${cand}_next_$found"]="$cur"
1222 | local curUsedBy="${S_DATA["${cur}_used by"]}"
1223 |
1224 | #find further hops starting from there
1225 | [ -n "$curUsedBy" ] && statusAllFindNextHop "$cur" "$curDesc"
1226 | fi
1227 | done
1228 |
1229 | #set exit code
1230 | [ $found -ne 0 ]
1231 | }
1232 |
1233 | #statusAllPrint [last hop] [previous hop string] [previous status string]
1234 | #Generates the print output for statusAll.
1235 | #[last hop]: The hop from which to start.
1236 | #[previous hop string]: Output generated from previous recursions.
1237 | #[previous status string]: Output generated from previous recursions.
1238 | #returns: Nothing.
1239 | function statusAllPrint {
1240 | local lastHop="$1"
1241 | local prevHopString="$2"
1242 | local prevStatString="$3"
1243 |
1244 | #there can only be a single next hop or none --> find it
1245 | local i=1
1246 | local nextHop=""
1247 | while : ; do
1248 | #NOTE: we need to check for existence as some hops may have been pruned using ""
1249 | if [ -n "${S_HOPS["${lastHop}_next_$i"]+exists}" ] ; then
1250 | nextHop="${S_HOPS["${lastHop}_next_$i"]}"
1251 | [ -n "$nextHop" ] && break
1252 | else
1253 | break
1254 | fi
1255 | i=$(( $i +1 ))
1256 | done
1257 |
1258 | #update print strings
1259 | if [ -z "$prevHopString" ] ; then
1260 | prevHopString="$lastHop"
1261 | local key="${S_DATA["${nextHop}_description"]}"
1262 | [ -z "$key" ] && key="UNKNOWN-KEY" #maybe TODO: retrieve the correct key from the mapper name of any destination VM (risk: VM --> dom0 data flow)
1263 | printf -v prevStatString 'qcrypt status -- %q %q %q' "${S_DATA["${lastHop}_backend"]}" "${S_DATA["${lastHop}_description"]}" "$key"
1264 | else
1265 | prevHopString="$prevHopString --> $lastHop"
1266 | printf -v prevStatString '%s %q' "$prevStatString" "${S_DATA["${lastHop}_backend"]}"
1267 | fi
1268 |
1269 | #we are the last hop
1270 | if [ -z "$nextHop" ] ; then
1271 | #special case: the last hop might actually be used by someone (without any further reference in S_HOPS) and if so, we need to add that VM as "real" last hop
1272 | local usedBy="${S_DATA["${lastHop}_used by"]}"
1273 | if [ -n "$usedBy" ] ; then
1274 | #NOTE: we simply use the frontend-dev in that case to avoid another call to the VM
1275 | prevHopString="$prevHopString --> $usedBy:${S_DATA["${lastHop}_frontend-dev"]}"
1276 | printf -v prevStatString "%s %q" "$prevStatString" "$usedBy"
1277 | fi
1278 |
1279 | local ro="${S_DATA["${lastHop}_read-only"]}"
1280 | [[ "$ro" == "yes" ]] && ro="r/o" || ro="r/w"
1281 |
1282 | #print
1283 | echo "$prevHopString ($ro)"
1284 | echo "$prevStatString"
1285 | echo ""
1286 | return 0
1287 | else
1288 | #find next hops
1289 | statusAllPrint "$nextHop" "$prevHopString" "$prevStatString"
1290 | fi
1291 | }
1292 |
1293 | #cryptoDeviceMatches [encrypted device] [decrypted device]
1294 | #Check whether the given encrypted device corresponds to the given decrypted device.
1295 | #[encrypted device]: Name of the encrypted luks device without /dev/.
1296 | #[decrypted device]: Name of the decrypted luks device without /dev/, e.g. dm-3.
1297 | #returns: 0 if and only if the devices match and no error occurred.
1298 | function cryptoDeviceMatches {
1299 | local encrypted="/dev/$1"
1300 | local decrypted="/dev/$2"
1301 | local out=
1302 | out="$(cryptsetup status "$decrypted" 2> /dev/null)" || return 1
1303 |
1304 | local re='^[[:space:]]*device:[[:space:]]*'"$encrypted"'[[:space:]]*$'
1305 | while b_readLine ; do
1306 | [[ "$B_LINE" =~ $re ]] && return 0
1307 | done <<< "$out"
1308 |
1309 | return 2
1310 | }
1311 |
1312 | #see status @usage
1313 | #@B_E
1314 | function statusAllC {
1315 | local qvmBlockInfo=""
1316 | qvmBlockInfo="$(b_dom0_parseQvmBlock "S_BLOCK")" || { B_ERR="Failed to parse qvm-block ls." ; B_E ; }
1317 | qvmBlockInfo="${qvmBlockInfo/declare -A/declare -gA}"
1318 | eval "$qvmBlockInfo" || { B_ERR="b_dom0_parseQvmBlock output incorrect." ; B_E ; }
1319 |
1320 | #example qvm-block ls output:
1321 | #result of `qcrypt open -- disp287 /tmp/foo4 foo4 testing-pers d-testing`
1322 | #BACKEND:DEVID DESCRIPTION USED BY
1323 | #disp287:loop0 /tmp/foo4 testing-pers (read-only=no, frontend-dev=xvdi)
1324 | #testing-pers:dm-0 foo4 d-testing (read-only=no, frontend-dev=xvdi)
1325 | #d-testing:dm-0 foo4 <--- line sometimes missing!
1326 | #foo4 is the /dev/mapper/foo4 name inside testing-pers providing a symlink to /dev/dm-0 and used by d-testing as /dev/xvdi
1327 | #--> rough algorithm GEN_PATHS (forward search):
1328 | # 1. For all BACKEND:DEVIDs with DEVID not matching ^dm\-[0-9]+$ and non-empty USED BY:
1329 | # a. Check whether their USED BY leads to a VM with a DEVID matching ^dm\-[0-9]+$ and has a non-empty DESCRIPTION. Save those candidates.
1330 | # b. Repeat a. (recurse?) for all the saved candidates until no further link is available. The resulting chains are saved as candidate chains.
1331 | # 2. Prune all candidate chains where the descriptions apart from the first VM are not identical or check that as part of 1.b.
1332 | # 3. Prune candidates: Inside the first intermediary VM get the encrypted device corresponding to the dm-X container (Warning: untrusted input coming from the VM). Prune, if that doesn't match the frontend-device of the source VM of that candidate chain.
1333 |
1334 | #find potential start points
1335 | #ideas:
1336 | # - save all start points inside S_CAND
1337 | # - save all required VM data inside S_DATA
1338 | # - save hop data inside S_HOPS; this should make further traversal rather simple
1339 | local id=
1340 | local i=
1341 | for ((i=0; i < ${S_BLOCK["max"]}; i++)) ; do
1342 | id="${S_BLOCK["${i}_id"]}"
1343 |
1344 | #create the data array
1345 | S_DATA["${id}_backend"]="${S_BLOCK["${i}_backend"]}"
1346 | S_DATA["${id}_device id"]="${S_BLOCK["${i}_device id"]}"
1347 | S_DATA["${id}_description"]="${S_BLOCK["${i}_description"]}"
1348 | S_DATA["${id}_used by"]="${S_BLOCK["${i}_used by"]}"
1349 | S_DATA["${id}_frontend-dev"]="${S_BLOCK["${i}_frontend-dev"]}"
1350 | S_DATA["${id}_read-only"]="${S_BLOCK["${i}_read-only"]}"
1351 |
1352 | #save candidates
1353 | if [ -n "${S_BLOCK["${i}_used by"]}" ] && [ -n "${S_BLOCK["${i}_frontend-dev"]}" ] && [[ ! "${S_BLOCK["${i}_device id"]}" =~ ^dm\-[0-9]+$ ]] ; then
1354 | S_CAND+=("$id")
1355 | fi
1356 | done
1357 |
1358 | #find all available next hops
1359 | local cand=
1360 | for cand in "${S_CAND[@]}" ; do
1361 | statusAllFindNextHop "$cand"
1362 | done
1363 |
1364 | #prune according to 3.
1365 | local nextHop=
1366 | local nextHopCrypt=
1367 | for cand in "${S_CAND[@]}" ; do
1368 | local j=1
1369 | local candFrontend="${S_DATA["${cand}_frontend-dev"]}"
1370 | [ -z "$candFrontend" ] && B_ERR="No frontend device found for $cand. Programming error?!" && B_E
1371 |
1372 | while : ; do
1373 | if [ -n "${S_HOPS["${cand}_next_$j"]+exists}" ] ; then
1374 | nextHop="${S_HOPS["${cand}_next_$j"]}"
1375 | nextHopCrypt="${S_DATA["${nextHop}_device id"]}"
1376 | #NOTE: we do not retrieve data to dom0 for security reasons (except for the status code)
1377 | if ! b_silence b_dom0_execFuncIn "${S_DATA["${nextHop}_backend"]}" "" "cryptoDeviceMatches" - - "$candFrontend" "$nextHopCrypt" ; then
1378 | S_HOPS["${cand}_next_$j"]=""
1379 | fi
1380 | else
1381 | break
1382 | fi
1383 |
1384 | j=$(( $j +1 ))
1385 | done
1386 | done
1387 |
1388 | #print chains
1389 | for cand in "${S_CAND[@]}" ; do
1390 | statusAllPrint "$cand"
1391 | done
1392 |
1393 | return 0
1394 | }
1395 |
1396 | checkDependencies
1397 | parseAndCheckArgs "$@"
1398 |
1399 | case "$(b_args_get 0)" in
1400 | "open")
1401 | openC
1402 | ;;
1403 |
1404 | "luksInit")
1405 | luksInitC
1406 | ;;
1407 |
1408 | "close")
1409 | closeC
1410 | ;;
1411 |
1412 | "status")
1413 | [ $# -gt 1 ] && statusSingleC || statusAllC
1414 | ;;
1415 |
1416 | *)
1417 | usage
1418 | ;;
1419 | esac
1420 |
--------------------------------------------------------------------------------
/qcryptd:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #
3 | #See usage().
4 | #
5 | #Copyright (C) 2020 David Hobach GPLv3
6 | #version: 0.9
7 | #
8 | #This program is free software: you can redistribute it and/or modify
9 | #it under the terms of the GNU General Public License as published by
10 | #the Free Software Foundation, either version 3 of the License, or
11 | #(at your option) any later version.
12 | #
13 | #This program is distributed in the hope that it will be useful,
14 | #but WITHOUT ANY WARRANTY; without even the implied warranty of
15 | #MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 | #GNU General Public License for more details.
17 | #
18 | #You should have received a copy of the GNU General Public License
19 | #along with this program. If not, see .
20 | #
21 |
22 | #init blib
23 | source blib
24 | b_checkVersion 1 6 || { >&2 echo "This script depends on blib (https://github.com/3hhh/blib) version 1.6 or higher. Please install a supported version." ; exit 1 ; }
25 | eval "$B_SCRIPT"
26 | b_import "arr"
27 | b_import "args"
28 | b_import "daemon"
29 | b_import "proc"
30 | b_import "ini"
31 | b_import "fs"
32 | b_import "notify"
33 | b_import "multithreading/mtx"
34 | b_import "os/qubes4/dom0"
35 |
36 | #path to the qcrypt binary
37 | QCRYPT="$B_SCRIPT_DIR/qcrypt"
38 |
39 | #main & secondary configuration directory
40 | QCRYPTD_CONF="/etc/$B_SCRIPT_NAME"
41 | QCRYPTD_CONF_2="$B_SCRIPT_DIR/conf"
42 |
43 | #distinguish the B_E exit code from the "normal" error
44 | #shellcheck disable=SC2034
45 | B_RC=6
46 |
47 | #default options for b_dom0_qvmRun & b_dom0_exec*
48 | #shellcheck disable=SC2034
49 | B_DOM0_QVM_RUN_PARAMS=("--no-gui")
50 |
51 | #daemon ID to uniquely identify the background process
52 | DID="$B_SCRIPT_NAME"
53 |
54 | #whether or not the daemon was initialized
55 | DAEMON_INIT_DONE=1
56 |
57 | #debug mode on/off switch (-v flag)
58 | DEBUG=1
59 |
60 | #where to log stdout & stderr daemon output, if DEBUG=0
61 | DEBUG_OUT="$B_SCRIPT_DIR/qcryptd.log"
62 | DEBUG_ERR="$DEBUG_OUT"
63 |
64 | #maximum number of lines for the debug log to keep upon initialization
65 | DEBUG_LINES_MAX=100000
66 |
67 | #used by the daemon to store the configuration target
68 | CONF_TARGET=
69 |
70 | #0 = the main loop should process events; everything else = it should exit
71 | PROCESS_EVENTS=0
72 |
73 | #qrexec timeout
74 | QREXEC_TIMEOUT="$(qubes-prefs "default_qrexec_timeout")" || { B_ERR="Failed to retrieve the default qrexec timeout." ; B_E ; }
75 |
76 | #internal configuration representation
77 | #array chains : all chains managed by qcryptd
78 | #map chains2info : chain ids --> all config input, chain start/stop command
79 | #map vms2cains : all VMs --> list of related chain ids
80 | #
81 | #terminology:
82 | #chain id: anything uniquely identifying a chain (can be the config file or a sourceVM:device:file combination)
83 | # here: config file (without .ini)
84 | #device: identified by its full path (recommended: /dev/disk/* ) inside a specifc VM, can be related to multiple chains (e.g. different file on the same device)
85 | # here: [source VM]:[source device]
86 | declare -ga CHAINS=()
87 | declare -gA CHAINS2INFO=()
88 | declare -gA VMS2CHAINS=()
89 |
90 | #maps tracking currently active devices, VMs and chains
91 | #ACTIVE_VMS / V: VM name --> 0/1/2 (2 = paused, use isRunning [vm] / isPaused [vm] to obtain the run state instead of this one)
92 | #ACTIVE_CHAINS / C: chain id --> 0/1 (use isActive [chain] to obtain the most recent state rather than this one)
93 | declare -gA ACTIVE_VMS=()
94 | declare -gA ACTIVE_CHAINS=()
95 |
96 | #helper maps for handleQubesEvent
97 | #LAST_ATTACHED: stores events and their times related to device attachments
98 | #LAST_START_ATTEMPT/LAST_SHUTDOWN_ATTEMPT: vm --> last start/shutdown attempt (not necessarily successful) timestamp in seconds since daemon start
99 | #SUPPRESSION: used to suppress the same event occurring in a very short time
100 | declare -gA LAST_ATTACHED=()
101 | declare -gA LAST_START_ATTEMPT=()
102 | declare -gA LAST_SHUTDOWN_ATTEMPT=()
103 | declare -gA SUPPRESSION=()
104 |
105 | function usage {
106 | echo "
107 | Usage: $B_SCRIPT_NAME [options] [command]
108 |
109 | Automatically manage qcrypt chains on VM starts/stops as well as device attachments.
110 |
111 | The daemon must be configured via ini files at the below configuration directory before it can be used. Each configuration file represents
112 | a single chain. You can find configuration examples inside the 'examples' directory.
113 |
114 | Configuration directories:
115 | $QCRYPTD_CONF
116 | $QCRYPTD_CONF_2
117 |
118 | [command] may be one of:
119 |
120 | start [target]
121 | Start the qcrypt daemon in the background.
122 |
123 | [target] Defines the configuration directory to use (default: default).
124 |
125 | Options:
126 | -v Start the daemon in verbose mode. This will produce output suitable for debugging, but possibly with a performance
127 | impact. Verbose mode is supported by all commands.
128 |
129 | stop
130 | Stop the running qcrypt daemon.
131 |
132 | Options:
133 | -c Close all qcrypt chains and their VMs before shutting the daemon down. This can also fix partially closed chains. By
134 | default, all qcrypt chains are left as-is. It is recommended to stop with -c before switching to a different config-
135 | uration.
136 | IMPORTANT: This will shut down all VMs for which the current target has qcrypt configurations - even VMs with inactive
137 | chains. So make sure to save any open data before!
138 |
139 | restart [target]
140 | Restart the qcrypt daemon. Configuration changes always require a daemon restart to become effective.
141 |
142 | [target] The target for the start operation (default: default).
143 |
144 | Options:
145 | -c See stop.
146 |
147 | check [target]
148 | Check the given target configuration for correctness. If no issues are found, a zero exit code is set. It is recommended to use this
149 | operation before deploying a new configuration.
150 |
151 | [target] The target to check (default: default).
152 |
153 | chains [target]
154 | Show the qcrypt status commands for all chains related to the given configuration target (default: default).
155 |
156 | Options:
157 | -n Don't display the configuration files.
158 | -e Execute the qcrypt status commands and provide a short summary for each. The qcryptd exit code indicates the number
159 | of chains in an invalid state.
160 |
161 | status
162 | Check whether a qcrypt daemon is running in the background. If you need information about the currently active qcrypt chains, please
163 | use the qcrypt status output. Sets a zero exit code, if a qcrypt daemon is running and a non-zero exit code otherwise.
164 |
165 | help
166 | print this help"
167 | exit 1
168 | }
169 |
170 | #parseIniKey [chain id] [key] [type] [required] [fallback]
171 | #Parse ini information to CHAINS2INFO.
172 | #[chain id]: Chain to do the parsing for.
173 | #[key]: Name of the ini key to parse.
174 | #[type]: 1=string, 2=bool, 3=int
175 | #[required]: whether the value is required (0) or not
176 | #[fallback]: fallback value to set if no value is specified (for the optional ones only) (default: empty)
177 | #returns: Prints errors and sets a non-zero exit code if they occur.
178 | function parseIniKey {
179 | local chainId="$1"
180 | local key="$2"
181 | local type="$3"
182 | local required="${4:-0}"
183 | local fallback="$5"
184 | local ret=0
185 |
186 | case $type in
187 | 1)
188 | CHAINS2INFO["${chainId}_$key"]="$(b_ini_getString "$key")"
189 | ret=$?
190 | ;;
191 | 2)
192 | CHAINS2INFO["${chainId}_$key"]="$(b_ini_getBool "$key")"
193 | ret=$?
194 | ;;
195 | 3)
196 | CHAINS2INFO["${chainId}_$key"]="$(b_ini_getInt "$key")"
197 | ret=$?
198 | ;;
199 | *)
200 | B_ERR="Unexpected type: $type"
201 | B_E
202 | esac
203 |
204 | case $ret in
205 | 0)
206 | #NOTE: the value may still be ""
207 | :
208 | ;;
209 | 1)
210 | if [ $required -eq 0 ] ; then
211 | echo "Missing: $key"
212 | return 1
213 | fi
214 | ;;
215 | 2)
216 | echo "Parsing error: $key"
217 | return 2
218 | ;;
219 | *)
220 | B_ERR="Unexpected exit code: $ret"
221 | B_E
222 | esac
223 |
224 | #set fallback if required
225 | if [ -z "${CHAINS2INFO["${chainId}_$key"]}" ] ; then
226 | if [ -n "$fallback" ] ; then
227 | CHAINS2INFO["${chainId}_$key"]="$fallback"
228 | elif [ $required -eq 0 ] ; then
229 | echo "Missing: $key"
230 | return 1
231 | fi
232 | fi
233 |
234 | return 0
235 | }
236 |
237 | #parseConfigFile [file]
238 | #Parse the given configuration file and update the current configuration state accordingly.
239 | #[file]: Full path to the ini file to parse.
240 | #returns: Nothing, but triggers [B_E](#B_E) on errors.
241 | #@B_E
242 | #@StateChanging
243 | function parseConfigFile {
244 | local file="$1"
245 | b_ini_read "$file" || { B_ERR="Failed to parse the file $file." ; B_E ; }
246 |
247 | local chainId="${file##*/}"
248 | chainId="${chainId%.ini}"
249 | [ -z "$chainId" ] && B_ERR="Empty chain ID. Programming mistake?!" && B_E
250 | CHAINS+=("$chainId")
251 | CHAINS2INFO["${chainId}_config file"]="$file"
252 |
253 | #populate parts of CHAINS2INFO
254 | local errCnt=0
255 | parseIniKey "$chainId" "source vm" 1 0 || ((errCnt+=1))
256 | parseIniKey "$chainId" "source device" 1 1 || ((errCnt+=1))
257 | parseIniKey "$chainId" "source mount point" 1 1 || ((errCnt+=1))
258 | parseIniKey "$chainId" "source file" 1 0 || ((errCnt+=1))
259 | parseIniKey "$chainId" "key" 1 0 || ((errCnt+=1))
260 | local i=
261 | for ((i=1;;i++)) ; do
262 | if [ $i -eq 1 ] ; then
263 | parseIniKey "$chainId" "destination vm $i" 1 0 || ((errCnt+=1))
264 | else
265 | #probe first, then parse
266 | (parseIniKey "$chainId" "destination vm $i" 1 0 &> /dev/null) || break
267 | parseIniKey "$chainId" "destination vm $i" 1 0
268 | fi
269 | parseIniKey "$chainId" "destination inj $i" 1 1 || ((errCnt+=1))
270 | parseIniKey "$chainId" "destination opt $i" 1 1 || ((errCnt+=1))
271 | done
272 | parseIniKey "$chainId" "destination mount point" 1 1 || ((errCnt+=1))
273 | parseIniKey "$chainId" "autostart" 2 1 "1" || ((errCnt+=1))
274 | #NOTE: read-only is set to true by default to prevent accidents (always make the more secure alternative the default)
275 | parseIniKey "$chainId" "read-only" 2 1 "0" || ((errCnt+=1))
276 | parseIniKey "$chainId" "startup interval" 3 1 "300" || ((errCnt+=1))
277 | parseIniKey "$chainId" "pre open command" 1 1 || ((errCnt+=1))
278 | parseIniKey "$chainId" "post open command" 1 1 || ((errCnt+=1))
279 | parseIniKey "$chainId" "pre close command" 1 1 || ((errCnt+=1))
280 | parseIniKey "$chainId" "post close command" 1 1 || ((errCnt+=1))
281 |
282 | #check for errors
283 | [ $errCnt -ne 0 ] && B_ERR="Exiting due to the above configuration errors." && B_E
284 |
285 | #validate keys
286 | local toCheck=("source vm" "source device" "source mount point" "source file" "key" "destination mount point" "autostart" "read-only" "startup interval" "pre open command" "post open command" "pre close command" "post close command")
287 | for ((i=1;i<=10;i++)) ; do
288 | toCheck+=("destination vm $i")
289 | toCheck+=("destination inj $i")
290 | toCheck+=("destination opt $i")
291 | done
292 | b_ini_assertNames "" "${toCheck[@]}"
293 |
294 | #generate open, close & status commands for the chain (required for initializeActiveChains)
295 | generateCommandsFor "$chainId"
296 |
297 | #add chain mutex
298 | CHAINS2INFO["${chainId}_mutex"]="$(b_mtx_create)"
299 |
300 | #populate VMS2CHAINS (may add duplicates for circular chains) & set CHAINS2INFO["${chainId}_final destination vm"]
301 | local vm="${CHAINS2INFO["${chainId}_source vm"]}"
302 | VMS2CHAINS["$vm"]="$chainId"$'\n'"${VMS2CHAINS["$vm"]}"
303 | for ((i=1;;i++)) ; do
304 | vm="${CHAINS2INFO["${chainId}_destination vm $i"]}"
305 | [ -z "$vm" ] && break
306 | VMS2CHAINS["$vm"]="$chainId"$'\n'"${VMS2CHAINS["$vm"]}"
307 | CHAINS2INFO["${chainId}_final destination vm"]="$vm"
308 | done
309 | }
310 |
311 | #generateCommandsFor [chain id]
312 | #Generates the relevant qcrypt commands for the given chain and stores them inside CHAINS2INFO.
313 | #Make sure to call it again whenever CHAINS2INFO changes (e.g. the source mount point happens to be a different one).
314 | #[chain id]: Chain to generate commands for.
315 | #returns: Nothing, but triggers [B_E](#B_E) on errors.
316 | #@B_E
317 | function generateCommandsFor {
318 | local chainId="$1"
319 |
320 | #generate generic argument chain common to all commands
321 | local srcFull="${CHAINS2INFO["${chainId}_source mount point"]}/${CHAINS2INFO["${chainId}_source file"]}"
322 | local argChain=
323 | printf -v argChain '%q %q %q' "${CHAINS2INFO["${chainId}_source vm"]}" "$srcFull" "${CHAINS2INFO["${chainId}_key"]}"
324 | local i=
325 | local vm=
326 | local options=""
327 | local inj=
328 | local copt=
329 | for ((i=1;;i++)) ; do
330 | vm="${CHAINS2INFO["${chainId}_destination vm $i"]}"
331 | [ -z "$vm" ] && break
332 | printf -v argChain '%s %q' "$argChain" "$vm"
333 |
334 | inj="${CHAINS2INFO["${chainId}_destination inj $i"]}"
335 | if [ -n "$inj" ] ; then
336 | printf -v options '%s --inj %q %q' "$options" "$vm" "$inj"
337 | fi
338 |
339 | copt="${CHAINS2INFO["${chainId}_destination opt $i"]}"
340 | if [ -n "$copt" ] ; then
341 | [[ "$copt" == *"'"* ]] && B_ERR="Found a disallowed single quote inside the cryptsetup option: $copt" && B_E
342 | printf -v options "%s --cy %q '%s'" "$options" "$vm" "$copt"
343 | fi
344 | done
345 |
346 | #open command
347 | local cmd=
348 | [ ${CHAINS2INFO["${chainId}_autostart"]} -eq 0 ] && options="$options -a"
349 | [ ${CHAINS2INFO["${chainId}_read-only"]} -eq 0 ] && options="$options --ro"
350 | local statusOptions=""
351 | [ -n "${CHAINS2INFO["${chainId}_destination mount point"]}" ] && printf -v options '%s --mp %q' "$options" "${CHAINS2INFO["${chainId}_destination mount point"]}" && printf -v statusOptions '%s --mp ""' "$statusOptions"
352 | printf -v cmd '%q %s open -- %s' "$QCRYPT" "$options" "$argChain"
353 | CHAINS2INFO["${chainId}_open"]="$cmd"
354 |
355 | #status command
356 | printf -v cmd '%q status%s -- %s' "$QCRYPT" "$statusOptions" "$argChain"
357 | CHAINS2INFO["${chainId}_status"]="$cmd"
358 |
359 | #close command
360 | printf -v cmd '%q close --force -- %s' "$QCRYPT" "$argChain"
361 | CHAINS2INFO["${chainId}_close"]="$cmd"
362 | }
363 |
364 | #getConfigFolder [target]
365 | #Get the configuration folder for the given target.
366 | #returns: The folder or errors out with [B_E](#B_E), if no such config folder exists.
367 | #@B_E
368 | function getConfigFolder {
369 | local target="${1:-default}"
370 | local targetFolder="$QCRYPTD_CONF/$target"
371 | [ -d "$targetFolder" ] && { echo "$targetFolder" ; return 0 ; } || targetFolder="$QCRYPTD_CONF_2/$target"
372 | [ -d "$targetFolder" ] && { echo "$targetFolder" ; return 0 ; } || { B_ERR="No configuration for the target $target found in either $QCRYPTD_CONF or $QCRYPTD_CONF_2." ; B_E ; }
373 | }
374 |
375 | #parseConfigFiles [target]
376 | #Parse all config files for the given target.
377 | #[target]: Configuration target.
378 | #returns: Nothing, but triggers [B_E](#B_E) on errors.
379 | #@B_E
380 | #@StateChanging
381 | function parseConfigFiles {
382 | local target="${1:-default}"
383 | local targetFolder=""
384 | targetFolder="$(getConfigFolder "$target")" || { B_ERR="The configuration for the target $target does not exist." ; B_E ; }
385 |
386 | local file=
387 | for file in "$targetFolder/"*.ini ; do
388 | if [ -f "$file" ] ; then
389 | parseConfigFile "$file"
390 | fi
391 | done
392 |
393 | #remove duplicates from VMS2CHAINS
394 | local list=
395 | local key=
396 | for key in "${!VMS2CHAINS[@]}" ; do
397 | list="${VMS2CHAINS["$key"]}"
398 | list="$(echo "$list" | sort -u)"
399 | VMS2CHAINS["$key"]="$list"
400 | done
401 |
402 | return 0
403 | }
404 |
405 | #see check @usage
406 | #@B_E
407 | function checkC {
408 | parseConfigFiles "$(b_args_get 1)"
409 |
410 | if [ $DEBUG -eq 0 ] ; then
411 | declare -p CHAINS
412 | declare -p CHAINS2INFO
413 | declare -p VMS2CHAINS
414 | fi
415 |
416 | echo "All good."
417 | exit 0
418 | }
419 |
420 | #see chains @usage
421 | #@B_E
422 | function chainsC {
423 | parseConfigFiles "$(b_args_get 1)"
424 | local execute=1
425 | b_args_getOption "-e" > /dev/null && execute=0
426 | local showConfigFiles=0
427 | b_args_getOption "-n" > /dev/null && showConfigFiles=1
428 |
429 | local ret=0
430 | local chain=
431 |
432 | local badChains=""
433 | if [ $execute -eq 0 ] ; then
434 | badChains="$(getBadChains "$(b_arr_toList "${CHAINS[@]}")" 2> /dev/null)" || { B_ERR="Failed to obtain the list of chains which are in a bad state." ; B_E ; }
435 | fi
436 |
437 | local prefix=""
438 | local format='%s%s'$'\n'
439 | [ $showConfigFiles -eq 0 ] && format='%-25s%s'$'\n'
440 | for chain in "${CHAINS[@]}" ; do
441 | [ $showConfigFiles -eq 0 ] && prefix="${CHAINS2INFO["${chain}_config file"]##*/}: "
442 | printf "$format" "$prefix" "${CHAINS2INFO["${chain}_status"]}"
443 | if [ $execute -eq 0 ] ; then
444 | local state="good"
445 | b_listContains "$badChains" "$chain" && state="bad" && ret=$(( $ret +1 ))
446 | printf ' state: %s'$'\n' "$state"
447 | fi
448 | done
449 |
450 | exit $ret
451 | }
452 |
453 | #see stop @usage
454 | #@B_E
455 | function stopC {
456 | #parse params
457 | local termSignal="SIGUSR1"
458 | b_args_getOption "-c" > /dev/null && termSignal="SIGUSR2"
459 |
460 | b_daemon_stop "$DID" "$termSignal" 0
461 | }
462 |
463 | #logError [message] [notify] [notification summary]
464 | #[notify]: If set to 0, send a user notification as well (default: 1)
465 | function logError {
466 | local msg="$1"
467 | local notify="${2:-1}"
468 | local summary="${3:-"$B_SCRIPT_NAME: ERROR"}"
469 | #NOTE: we write to stderr (which is logged in debug mode) to avoid conflicts with echoed return values from inside functions
470 | [ $DEBUG -eq 0 ] && >&2 echo "$SECONDS [$BASHPID] ERROR: $msg"
471 | logger -p daemon.err -t "$B_SCRIPT_NAME" "[$BASHPID] $msg (target: $CONF_TARGET)"
472 | if [ $notify -eq 0 ] ; then
473 | b_notify_sendNoError -u critical -t 60000 "$summary" "$msg" &
474 | disown
475 | fi
476 | return 0
477 | }
478 |
479 | #logInfo [message] [notify] [notification summary]
480 | #[notify]: If set to 0, send a user notification as well (default: 1)
481 | function logInfo {
482 | local msg="$1"
483 | local notify="${2:-1}"
484 | local summary="${3:-"$B_SCRIPT_NAME: INFO"}"
485 | #NOTE: we write to stderr (which is logged in debug mode) to avoid conflicts with echoed return values from inside functions
486 | [ $DEBUG -eq 0 ] && >&2 echo "$SECONDS [$BASHPID] INFO: $msg"
487 | logger -p daemon.notice -t "$B_SCRIPT_NAME" "[$BASHPID] $msg (target: $CONF_TARGET)"
488 | if [ $notify -eq 0 ] ; then
489 | b_notify_sendNoError "$summary" "$msg" &
490 | disown
491 | fi
492 | return 0
493 | }
494 |
495 | function logState {
496 | [ $DEBUG -eq 0 ] && echo $'\n'"STATE BEGIN"
497 | logInfo "$(declare -p CHAINS)"
498 | logInfo "$(declare -p CHAINS2INFO)"
499 | logInfo "$(declare -p VMS2CHAINS)"
500 | logInfo "$(declare -p ACTIVE_VMS)"
501 | logInfo "$(declare -p ACTIVE_CHAINS)"
502 | logInfo "$(declare -p LAST_ATTACHED)"
503 | logInfo "$(declare -p SUPPRESSION)"
504 | #logInfo "$(qvm-ls -q --no-spinner)"
505 | #logInfo "$(qvm-block ls)"
506 | [ $DEBUG -eq 0 ] && echo "STATE END"$'\n'
507 | }
508 |
509 | #loggingDaemonErrorHandler [error out]
510 | function loggingDaemonErrorHandler {
511 | local errorOut=${1:-0}
512 |
513 | #set the proper exit code
514 | if [ $errorOut -eq 0 ] ; then
515 | #only the daemon itself should cause FATALs
516 | if [ $BASHPID -eq $$ ] ; then
517 | [ $DEBUG -eq 0 ] && logState
518 | logError "FATAL: $B_ERR" 0
519 | logError "Daemon exiting..."
520 | else
521 | logError "$B_ERR Child thread exiting..."
522 | fi
523 | return 2
524 | else
525 | logInfo "$B_ERR"
526 | return 1
527 | fi
528 | }
529 |
530 | #isRunning [vm] [allow paused]
531 | #Check whether the given VM is running according to our internal state.
532 | #[allow paused]: If set to 0, also consider paused VMs to be running (default: 1).
533 | #returns: A zero exit code, if it is running and a nonzero exit code otherwise.
534 | function isRunning {
535 | local vm="$1"
536 | local allowPaused="${2:-1}"
537 | local ePaused=0
538 | [ $allowPaused -eq 0 ] && ePaused=2
539 | [[ -n "${ACTIVE_VMS["$vm"]}" && ( ${ACTIVE_VMS["$vm"]} -eq 0 || ${ACTIVE_VMS["$vm"]} -eq $ePaused ) ]]
540 | }
541 |
542 | #isPaused [vm]
543 | #Check whether the given VM is paused according to our internal state.
544 | #returns: A zero exit code, if it is running and a nonzero exit code otherwise.
545 | function isPaused {
546 | local vm="$1"
547 | [ -n "${ACTIVE_VMS["$vm"]}" ] && [ ${ACTIVE_VMS["$vm"]} -eq 2 ]
548 | }
549 |
550 | #chainVMsAreRunning [chain id]
551 | #Check whether all VMs of the given chain which are necessary to open it are running.
552 | #[chain id]: ID of a single chain.
553 | #returns: Sets a zero exit code, if all involved VMs are running and a non-zero exit code otherwise.
554 | function chainVMsAreRunning {
555 | local chain="$1"
556 |
557 | #check source VM & final destination VM (all intermediaries are autostarted by qcrypt)
558 | isRunning "${CHAINS2INFO["${chain}_source vm"]}" && isRunning "${CHAINS2INFO["${chain}_final destination vm"]}"
559 | }
560 |
561 | #prepareChainOpen [chain id]
562 | #Do all preparations necessary to open the given qcrypt chain, which _must_ run in the main thread/daemon as they are _not_ thread safe.
563 | #[chain id]: ID of a single chain.
564 | #returns: Sets a zero exit code, if the preparations were successful and a chain start may now proceed. Otherwise a non-zero exit code is set. Unexpected errors are logged.
565 | #@StateChanging
566 | function prepareChainOpen {
567 | #NOTE: qcrypt does any autostart, the started VMs will trigger new events and the daemon state will be updated accordingly --> nothing to do for that
568 | local chain="$1"
569 |
570 | local srcVM="${CHAINS2INFO["${chain}_source vm"]}"
571 | local srcDev="${CHAINS2INFO["${chain}_source device"]}"
572 | local srcMp="${CHAINS2INFO["${chain}_source mount point"]}"
573 |
574 | #special case: autostart enabled & source VM not running (yet)
575 | local ret=3
576 | if [ ${CHAINS2INFO["${chain}_autostart"]} -eq 0 ] && ! isRunning "$srcVM" ; then
577 | b_setBE 1
578 | b_dom0_ensureRunning "$srcVM"
579 | ret=$?
580 | b_resetErrorHandler 1
581 | if [ $ret -eq 0 ] ; then
582 | ACTIVE_VMS["$srcVM"]=0
583 | else
584 | logError "Failed to start the VM $srcVM. Original error: $B_ERR"
585 | B_ERR=""
586 | return $ret
587 | fi
588 | fi
589 |
590 | #mount source device, if necessary
591 | #NOTE: _not_ thread safe (e.g. b_dom0_mountIfNecessary for the same source VM & device)!
592 | if [ -n "$srcDev" ] && [ -n "$srcMp" ] && [[ "$srcMp" != "/" ]] ; then
593 | b_setBE 1
594 | #NOTE: for security reasons we enforce our mount point (we don't want to read the untrusted VM output and pass it to our internal state)
595 | b_silence b_dom0_mountIfNecessary "$srcVM" "$srcDev" "$srcMp" 0
596 | ret=$?
597 | b_resetErrorHandler 1
598 | [ $ret -ne 0 ] && logError "Failed to mount the source device $srcVM:$srcDev to $srcMp (chain $chain). Device not available? Original error: $B_ERR"
599 | B_ERR=""
600 | return $ret
601 | else
602 | #local file, opening should work
603 | return 0
604 | fi
605 | }
606 |
607 | #updateChainState [chain] [operation] [wait flag]
608 | #Updates the ACTIVE_CHAINS array for the given chain with the latest information available.
609 | #[operation]: 1=open, 0=close
610 | #[wait flag]: If set to 0, even wait for on-going operations to complete (default: 1).
611 | #returns: Nothing.
612 | function updateChainState {
613 | local chain="$1"
614 | local op="$2"
615 | local waitFlag="${3:-1}"
616 | local ret=
617 |
618 | local pid=
619 | [ $op -eq 1 ] && pid="${CHAINS2INFO["${chain}_last open pid"]}" || pid="${CHAINS2INFO["${chain}_last close pid"]}"
620 | [ -z "$pid" ] && return 0
621 |
622 | if [ $waitFlag -ne 0 ] ; then
623 | #is the background process still running?
624 | b_proc_childExists "$pid" && return 0
625 | fi
626 |
627 | if wait "$pid" ; then
628 | #we thought the chain open/closed anyway --> nothing to do (especially since we might have closed in the meantime)
629 | :
630 | else
631 | #the last chain open/close failed --> update the internal state
632 | ACTIVE_CHAINS["$chain"]=$op
633 | fi
634 | [ $op -eq 1 ] && CHAINS2INFO["${chain}_last open pid"]="" || CHAINS2INFO["${chain}_last close pid"]=""
635 |
636 | return 0
637 | }
638 |
639 | #isActive [chain] [wait flag]
640 | #Check whether the given chain is active according to the latest information.
641 | #Must be run in the foreground by the daemon itself.
642 | #[wait flag]: If set to 0, even wait for on-going operations to complete (default: 1).
643 | #returns: A zero exit code if and only if it is active according to the latest information.
644 | function isActive {
645 | local chain="$1"
646 | local waitFlag="$2"
647 | updateChainState "$chain" 0 "$waitFlag"
648 | updateChainState "$chain" 1 "$waitFlag"
649 | [ -n "${ACTIVE_CHAINS["$chain"]}" ] && [ ${ACTIVE_CHAINS["$chain"]} -eq 0 ]
650 | }
651 |
652 | #openSingleChain [chain id]
653 | #Open a single chain in the foreground. Helper function for [openChains](#openChains), which must be used over this one.
654 | #returns: An exit code of 0, if and only if the chain was successfully opened. Errors are logged.
655 | #@B_E
656 | function openSingleChain {
657 | local chain="$1"
658 |
659 | #make sure previous operations are completed
660 | local release=
661 | release="$(b_mtx_waitFor "${CHAINS2INFO["${chain}_mutex"]}" "$BASHPID")" || { B_ERR="Failed to obtain a mutex." ; B_E ; }
662 | #shellcheck disable=SC2064
663 | trap "$release" EXIT RETURN
664 |
665 | #execute the pre open command
666 | local cmd="${CHAINS2INFO["${chain}_pre open command"]}"
667 | if [ -n "$cmd" ] ; then
668 | eval "$cmd" || { B_ERR="The pre open command for the chain $chain returned a non-zero exit code. Thus aborting the chain open process." ; B_E ; }
669 | fi
670 |
671 | #open the chain
672 | cmd="${CHAINS2INFO["${chain}_open"]}"
673 | [ -z "$cmd" ] && B_ERR="Missing open command for chain $chain. Programming error?!" && B_E
674 | local notifySummary="$B_SCRIPT_NAME status: $chain"
675 | if ! eval "$cmd" ; then
676 | local msg="Failed to open the chain $chain. Closing again..."
677 | logError "$msg" 0 "$notifySummary"
678 |
679 | #NOTES:
680 | # - failed open operations often result in partially open chains --> thus we attempt to close
681 | # - we don't need to use mutexes etc. as we still have the open mutex
682 | closeSingleChainNaively "$chain"
683 |
684 | B_ERR="$msg"
685 | B_E
686 | fi
687 |
688 | #execute the post open command
689 | cmd="${CHAINS2INFO["${chain}_post open command"]}"
690 | [ -n "$cmd" ] && eval "$cmd"
691 |
692 | logInfo "Opened the chain $chain." 0 "$notifySummary"
693 | return 0
694 | }
695 |
696 | #openChains [wait flag] [chain id 1] ... [chain id n]
697 | #[wait flag]: If set to 0, wait for on-going chain status changes possibly affecting our decision to open a chain.
698 | #Check whether it makes sense to open the given chains and if so, attempt it.
699 | #returns: Nothing. Errors are logged.
700 | function openChains {
701 | local waitFlag="$1"
702 | shift
703 |
704 | local chain=
705 | for chain in "$@" ; do
706 | #filter active chains
707 | isActive "$chain" "$waitFlag" && continue
708 |
709 | #filter chains with shut down VMs
710 | if [ ${CHAINS2INFO["${chain}_autostart"]} -ne 0 ] ; then
711 | chainVMsAreRunning "$chain" || continue
712 | fi
713 |
714 | #do not attempt to open chains with on-going close operation
715 | local last="${CHAINS2INFO["${chain}_last close pid"]}"
716 | [ -n "$last" ] && continue
717 |
718 | #do not open too often to prevent continuous re-open attempts in case of errors
719 | local last="${CHAINS2INFO["${chain}_last open"]}"
720 | [ -n "$last" ] && [ $(( $SECONDS - $last )) -le ${CHAINS2INFO["${chain}_startup interval"]} ] && continue
721 |
722 | #prepare (not thread safe, so must be run here)
723 | prepareChainOpen "$chain" || continue
724 |
725 | #open in background
726 | #NOTE: we aggressively assume that the chain goes active in the background, but re-check that assumption with isActive whenever needed
727 | logInfo "Attempting to open the chain $chain..."
728 | openSingleChain "$chain" < /dev/null &
729 | ACTIVE_CHAINS["$chain"]=0
730 | CHAINS2INFO["${chain}_last open"]="$SECONDS"
731 | CHAINS2INFO["${chain}_last open pid"]="$!"
732 | done
733 |
734 | return 0
735 | }
736 |
737 | #openChainsByVM [v] [all]
738 | #Attempt to open all chains related to the given VM.
739 | #An implementation of openChainsByVM(v,all).
740 | #[v]: A VM name.
741 | #[all]: all VMs are relevant (default, 0); if set to 1, only source VMs are relevant
742 | #returns: Nothing. Startup errors not caused by parts of the chain not being available will be logged.
743 | function openChainsByVM {
744 | local vm="$1"
745 | local all="${2:-0}"
746 |
747 | #get the applicable chains
748 | local chains="${VMS2CHAINS["$vm"]}"
749 |
750 | local chain=
751 | local toOpen=()
752 | while b_readLine chain ; do
753 | [ -z "$chain" ] && continue
754 | [ $all -ne 0 ] && [[ "$vm" != "${CHAINS2INFO["${chain}_source vm"]}" ]] && continue
755 | toOpen+=("$chain")
756 | done <<< "$chains"
757 |
758 | openChains 1 "${toOpen[@]}"
759 | }
760 |
761 | #closeSingleChainNaively [chain]
762 | #Close a single chain in the foreground naively (no checks, mutex obtaining, ... whatsoever). Helper function only. Usually [closeChains](#closeChains) should be used.
763 | #returns: An exit code of 0, if and only if the chain was successfully closed.
764 | #@B_E
765 | function closeSingleChainNaively {
766 | local chain="$1"
767 | local cmd="${CHAINS2INFO["${chain}_close"]}"
768 | [ -z "$cmd" ] && B_ERR="Missing close command for chain $chain. Programming error?!" && B_E
769 | eval "$cmd" || logError "Failed to correctly close the chain $chain. Assuming it closed anyway and proceeding..."
770 | return 0
771 | }
772 |
773 | #closeSingleChain [chain id] [force flag]
774 | #Close a single chain in the foreground. Helper function for [closeChains](#closeChains), which must be used over this one.
775 | #returns: An exit code of 0, if and only if the chain was successfully closed.
776 | #@B_E
777 | function closeSingleChain {
778 | local chain="$1"
779 | local forceFlag="${2:-1}"
780 |
781 | if [ $forceFlag -ne 0 ] ; then
782 | #only close chains whose final destination VM is down & warn the user about others
783 | #Reason: The close would shut down the destination VM, but the user might still be working inside it.
784 | #we double check with hasRecentShutdownAttempt() & b_dom0_isRunning() as multiple VMs may have been shut down at the same time and we received only the first few events so far
785 | #(in particular VMs shut down from inside the VM / not via qvm-shutdown appear to require the b_dom0_isRunning check)
786 | local final="${CHAINS2INFO["${chain}_final destination vm"]}"
787 | b_setBE 1
788 | if isRunning "$final" 0 && ! hasRecentShutdownAttempt "$final" && sleep 2 && b_dom0_isRunning "$final" &> /dev/null ; then
789 | logError "The qcrypt chain $chain is not working anymore and should be closed. However it seems that the $final VM is still running."$'\n'"Please shut it down manually to fix the issue. qcryptd should close the chain afterwards." 0
790 | exit 3
791 | fi
792 | b_resetErrorHandler
793 | fi
794 |
795 | #make sure previous operations are completed
796 | local release=
797 | release="$(b_mtx_waitFor "${CHAINS2INFO["${chain}_mutex"]}" "$BASHPID")" || { B_ERR="Failed to obtain a mutex." ; B_E ; }
798 | #shellcheck disable=SC2064
799 | trap "$release" EXIT RETURN
800 |
801 | #execute the pre close command
802 | local cmd="${CHAINS2INFO["${chain}_pre close command"]}"
803 | [ -n "$cmd" ] && eval "$cmd"
804 |
805 | #close the chain
806 | closeSingleChainNaively "$chain"
807 |
808 | #execute the post close command
809 | cmd="${CHAINS2INFO["${chain}_post close command"]}"
810 | [ -n "$cmd" ] && eval "$cmd"
811 |
812 | logInfo "Closed the chain $chain." 0 "$B_SCRIPT_NAME status: $chain"
813 | return 0
814 | }
815 |
816 | #closeChains [force flag] [wait flag] [chain id 1] ... [chain id n]
817 | #[force flag]: Force a close attempt regardless of the current state. May shut down VMs. Default: 1
818 | #[wait flag]: If set to 0, wait for on-going chain status changes possibly affecting our decision to close a chain. Default: 1
819 | #returns: Nothing.
820 | function closeChains {
821 | local forceFlag="${1:-1}"
822 | local waitFlag="${2:-1}"
823 | shift 2
824 |
825 | local chain=
826 | for chain in "$@" ; do
827 | #filter inactive chains
828 | [ $forceFlag -ne 0 ] && ! isActive "$chain" "$waitFlag" && continue
829 |
830 | #close in background
831 | #NOTE: this will shut down _ALL_ destination VMs in that chain
832 | logInfo "Attempting to close the chain $chain..."
833 | closeSingleChain "$chain" "$forceFlag" < /dev/null &
834 |
835 | #NOTE: the next open will wait for the last close and another overlapping close is not possible since the chain is now marked inactive
836 | CHAINS2INFO["${chain}_last close"]="$SECONDS"
837 | CHAINS2INFO["${chain}_last close pid"]="$!"
838 | ACTIVE_CHAINS["$chain"]=1
839 | done
840 | return 0
841 | }
842 |
843 | #getBadChains [list]
844 | #Check the status of all given chains and filter the chain list to only contain the ones with a bad status.
845 | #[list] List of chains to check the status for.
846 | #returns: Filtered list of chains. Errors are logged.
847 | function getBadChains {
848 | local list="$1"
849 | local chain=
850 | local cmd=
851 | #chain id --> pid
852 | declare -A pids=()
853 |
854 | while b_readLine chain ; do
855 | [ -z "$chain" ] && continue
856 | #NOTE: all VMs are likely running (we only get here for device attachments), no need to check
857 |
858 | #the open operation might not have completed yet
859 | local pid="${CHAINS2INFO["${chain}_last open pid"]}"
860 | if [ -n "$pid" ] ; then
861 | if b_proc_childExists "$pid" ; then
862 | [ $DEBUG -eq 0 ] && logInfo "$chain: Chain open still on-going @${pid}."
863 | continue
864 | else
865 | CHAINS2INFO["${chain}_last open pid"]=""
866 | fi
867 | fi
868 |
869 | cmd="${CHAINS2INFO["${chain}_status"]}"
870 | [ -z "$cmd" ] && B_ERR="Missing status command for chain $chain. Programming error?!" && B_E
871 | [ $DEBUG -eq 0 ] && logInfo "$chain: Checking the status...."
872 | eval "$cmd" &> /dev/null &
873 | pids["$chain"]=$!
874 | done <<< "$list"
875 |
876 | for chain in "${!pids[@]}" ; do
877 | if wait "${pids["$chain"]}" ; then
878 | [ $DEBUG -eq 0 ] && logInfo "$chain: Good status."
879 | else
880 | echo "$chain"
881 | fi
882 | done
883 |
884 | return 0
885 | }
886 |
887 | #filterChainsWithPausedDest [list]
888 | #[list]: List of chains.
889 | #returns: The input list; those chains with a paused destination VM were removed.
890 | function filterChainsWithPausedDest {
891 | local list="$1"
892 |
893 | local chain=
894 | while b_readLine chain ; do
895 | [ -z "$chain" ] && continue
896 | local final="${CHAINS2INFO["${chain}_final destination vm"]}"
897 | if isPaused "$final" ; then
898 | [ $DEBUG -eq 0 ] && logInfo "$chain: The final destination VM $final appears to be paused. Ignoring."
899 | else
900 | echo "$chain"
901 | fi
902 | done <<< "$list"
903 |
904 | return 0
905 | }
906 |
907 | #closeChainsByVM [v] [shutdown flag] [paused flag]
908 | #Close all chains related to the given VM.
909 | #An implementation of closeChainsByVM(v,VM shutdown=0|1).
910 | #[v]: A VM name.
911 | #[shutdown flag]: If the VM [v] was shut down and the chain requires closing for that reason (0) or the chains are expected to have other issues (e.g. device detached) (default: 1).
912 | #[paused flag]: If the VM [v] was paused and the chain requires closing for that reason (0) or the chains are expected to have other issues (e.g. device detached) (default: 1).
913 | #returns: Nothing. Close errors will be logged.
914 | function closeChainsByVM {
915 | local vm="$1"
916 | local shutdown="${2:-1}"
917 | local paused="${3:-1}"
918 |
919 | #get the applicable chains
920 | local chains=
921 | if [ $shutdown -eq 0 ] ; then
922 | chains="${VMS2CHAINS["$vm"]}"
923 | elif [ $paused -eq 0 ] ; then
924 | #special case: do not consider the chain broken, if the final destination VM is paused
925 | #reason: it'll fail, but in fact qcryptd should be alright once the user unpauses the VM --> unclear state --> do nothing now and check again on unpause
926 | #for paused intermediate VMs we still error out (unless their dest is paused) as that may cause data loss otherwise
927 | chains="$(filterChainsWithPausedDest "${VMS2CHAINS["$vm"]}")"
928 | else
929 | chains="$(filterChainsWithPausedDest "${VMS2CHAINS["$vm"]}")" #see above
930 | chains="$(getBadChains "$chains")"
931 | fi
932 |
933 | [ $DEBUG -eq 0 ] && logInfo "Chains about to be closed (if active):"$'\n'"$chains"$'\n'
934 |
935 | local chain=
936 | local toClose=()
937 | while b_readLine chain ; do
938 | [ -z "$chain" ] && continue
939 | toClose+=("$chain")
940 | done <<< "$chains"
941 |
942 | closeChains 1 1 "${toClose[@]}"
943 |
944 | return 0
945 | }
946 |
947 | #initializeActiveVMs
948 | #Initialize the ACTIVE_VMS map with the currently active VMs.
949 | #returns: A zero exit code on success and a non-zero exit code otherwise. Unexpected errors will trigger [B_E](#B_E).
950 | #@B_E
951 | #@StateChanging
952 | function initializeActiveVMs {
953 | local running=
954 | running="$(qvm-ls --running -O NAME --raw-list)" || { B_ERR="Faled to execute qvm-ls." ; B_E ; }
955 |
956 | local vm=
957 | while b_readLine vm ; do
958 | [ -z "$vm" ] && continue
959 | [[ "$vm" == "dom0" ]] && continue
960 | ACTIVE_VMS["$vm"]=0
961 | done <<< "$running"
962 |
963 | local paused=
964 | paused="$(qvm-ls --paused -O NAME --raw-list)" || { B_ERR="Faled to execute qvm-ls." ; B_E ; }
965 |
966 | while b_readLine vm ; do
967 | [ -z "$vm" ] && continue
968 | [[ "$vm" == "dom0" ]] && continue
969 | ACTIVE_VMS["$vm"]=2
970 | done <<< "$paused"
971 |
972 | return 0
973 | }
974 |
975 | #initializeActiveChains
976 | #Initialize the ACTIVE_CHAINS map with the currently active qcrypt chain ids.
977 | #Currently depends on a previous run of [initializeActiveVMs](#initializeActiveVMs).
978 | #returns: A zero exit code on success and a non-zero exit code otherwise. Unexpected errors will trigger [B_E](#B_E).
979 | #@B_E
980 | #@StateChanging
981 | function initializeActiveChains {
982 | local chain=
983 | local cmd=
984 | for chain in "${CHAINS[@]}" ; do
985 | cmd="${CHAINS2INFO["${chain}_status"]}"
986 | [ -z "$cmd" ] && B_ERR="Failed to find the status command. Programming error?!" && B_E
987 | chainVMsAreRunning "$chain" && eval "$cmd" && ACTIVE_CHAINS["$chain"]=0
988 | done
989 | return 0
990 | }
991 |
992 | #needsSuppression [vm] [event name] [event time]
993 | #Check whether the given event requires to be suppressed as it did recently occur already.
994 | #[event time]: in seconds since EPOCH
995 | #returns: A zero exit code, if the event should be suppressed.
996 | function needsSuppression {
997 | local vm="$1"
998 | local eventName="$2"
999 | local timestamp="$3"
1000 | local key="${vm}_${eventName}"
1001 |
1002 | local last="${SUPPRESSION["$key"]}"
1003 | SUPPRESSION["$key"]="$timestamp"
1004 |
1005 | echo ""
1006 | echo "$SECONDS suppression check: $vm: $eventName"
1007 | echo "last: $last"
1008 | echo "cur: $timestamp"
1009 | echo ""
1010 |
1011 | #suppress all events from less than a few seconds ago (same name, same VM)
1012 | [ -n "$last" ] && [ $(( $timestamp - $last )) -le 3 ]
1013 | }
1014 |
1015 | #hasRecentStartAttempt [vm]
1016 | #Check whether the given VM was seen with a recent start attempt.
1017 | #returns: A zero exit code, if a recent start attempt was seen for the VM.
1018 | function hasRecentStartAttempt {
1019 | local vm="$1"
1020 | [ -n "${LAST_START_ATTEMPT["$vm"]}" ] && [ $(( $SECONDS - ${LAST_START_ATTEMPT["$vm"]} )) -le $QREXEC_TIMEOUT ]
1021 | }
1022 |
1023 | #hasRecentShutdownAttempt [vm]
1024 | #Check whether the given VM was seen with a recent shutdown attempt.
1025 | #returns: A zero exit code, if a recent shutdown attempt was seen for the VM.
1026 | function hasRecentShutdownAttempt {
1027 | local vm="$1"
1028 | [ -n "${LAST_SHUTDOWN_ATTEMPT["$vm"]}" ] && [ $(( $SECONDS - ${LAST_SHUTDOWN_ATTEMPT["$vm"]} )) -le $QREXEC_TIMEOUT ]
1029 | }
1030 |
1031 | #onQubesEvent [subject] [event name] [event info] [timestamp]
1032 | #Called for every Qubes OS event.
1033 | #[subject]: The subject name Qubes OS provides. Usually the VM for which the event was reported. 'None' appears to mean 'dom0'.
1034 | #[event name]: Name of the event for which the callback function was called.
1035 | #[event info]: May contain additional information about the event (e.g. arguments).
1036 | #[timestamp]: When the event was received in ms since EPOCH.
1037 | #returns: Nothing. A non-zero exit code will abort further processing.
1038 | function onQubesEvent {
1039 | local vm="$1"
1040 | local eventName="$2"
1041 | local eventInfo="$3"
1042 | local timeSeconds="${4:0:10}"
1043 |
1044 | [ $PROCESS_EVENTS -ne 0 ] && return $PROCESS_EVENTS
1045 |
1046 | #NOTE: We only do some pre-filtering and (possibly) delaying here and leave the handling to [handleQubesEvent](#handleQubesEvent).
1047 |
1048 | #init if necessary
1049 | #NOTE: some things require initialization _in_ the event loop as we might otherwise lose events in the meantime
1050 | if [ $DAEMON_INIT_DONE -ne 0 ] ; then
1051 | logInfo "$(date) Initializing..."
1052 | initializeActiveVMs
1053 | initializeActiveChains
1054 | openChains 1 "${CHAINS[@]}"
1055 |
1056 | logInfo "Initialized."
1057 | [ $DEBUG -eq 0 ] && logState
1058 | DAEMON_INIT_DONE=0
1059 | fi
1060 |
1061 | handleQubesEvent "$vm" "$eventName" "$eventInfo" "$timeSeconds" || return $?
1062 | return 0
1063 | }
1064 |
1065 | #+handleQubesEvent [subject] [event name] [event info] [timestamp in seconds]
1066 | #+Handles Qubes events for [onQubesEvent](#onQubesEvent).
1067 | #+returns: Nothing.
1068 | function handleQubesEvent {
1069 | local vm="$1"
1070 | local eventName="$2"
1071 | local timeSeconds="$4"
1072 |
1073 | #handle the current event
1074 | case "$eventName" in
1075 | #VM start attempt
1076 | "domain-pre-start")
1077 | #update start attempt timestamp
1078 | LAST_START_ATTEMPT["$vm"]=$SECONDS
1079 | ;;
1080 | #VM started
1081 | "domain-start")
1082 | ACTIVE_VMS["$vm"]=0
1083 | unset 'LAST_START_ATTEMPT["$vm"]'
1084 |
1085 | #react
1086 | #(no suppression needed, there's just one event)
1087 | logInfo "REACTION: VM started: $vm"
1088 | openChainsByVM "$vm"
1089 | ;;
1090 | #VM shutdown attempt (not triggered on kill & not on shutdown from internal)
1091 | "domain-pre-shutdown")
1092 | #update shutdown attempt timestamp
1093 | LAST_SHUTDOWN_ATTEMPT["$vm"]=$SECONDS
1094 | ;;
1095 | #VM forced shutdown (on kill or on shutdown from internal), at least somewhat before "domain-shutdown"
1096 | "domain-stopped")
1097 | #update shutdown attempt timestamp
1098 | LAST_SHUTDOWN_ATTEMPT["$vm"]=$SECONDS
1099 | ;;
1100 | #VM stopped (triggered on shutdown & kill)
1101 | "domain-shutdown")
1102 | ACTIVE_VMS["$vm"]=1
1103 | unset 'LAST_SHUTDOWN_ATTEMPT["$vm"]'
1104 |
1105 | #some state cleanup
1106 | unset 'SUPPRESSION["${vm}_device-list-change:block"]'
1107 |
1108 | #react
1109 | #(no suppression needed, there's just one event)
1110 | logInfo "REACTION: VM stopped: $vm"
1111 | closeChainsByVM "$vm" 0
1112 | ;;
1113 | #VM paused
1114 | "domain-paused")
1115 | ACTIVE_VMS["$vm"]=2
1116 |
1117 | #react
1118 | #(no suppression needed, there's just one event)
1119 | logInfo "REACTION: VM paused: $vm"
1120 | closeChainsByVM "$vm" 1 0
1121 | ;;
1122 | #VM started OR unpaused
1123 | "domain-unpaused")
1124 | #attempt to trigger on VM unpauses _not_ triggered by starts only
1125 | if ! hasRecentStartAttempt "$vm" ; then
1126 | ACTIVE_VMS["$vm"]=0
1127 |
1128 | #react
1129 | #(no suppression needed, there's just one event)
1130 | logInfo "REACTION: VM unpaused: $vm"
1131 | openChainsByVM "$vm"
1132 | #special case: double check whether anything needs to be closed as something might have gone wrong during the pause (cf. special case @getBadChains())
1133 | closeChainsByVM "$vm"
1134 | fi
1135 | ;;
1136 | #device attach or detach
1137 | #for USB devices the following appears to hold:
1138 | # Device detached: one "device-list-change:block" event followed by one "device-list-change:usb" event in max 2 seconds
1139 | # Device attached: one "device-list-change:usb" event followed by one "device-list-change:block" event in max 2 seconds
1140 | #for others (cloud mounts, ...) however we'll probably just see a single device-list-change:block event for *both* attach *and* detach
1141 | # --> we just try to open and close all chains related to that VM (may be expensive)
1142 | # if Qubes OS ever gets some more details into its events, we could change that
1143 | "device-list-change:block")
1144 | needsSuppression "$vm" "$eventName" "$timeSeconds" && return 0
1145 |
1146 | #sometimes also triggered on shutdown
1147 | if isRunning "$vm" ; then
1148 | #react
1149 | logInfo "REACTION: Device attached or detached: $vm"
1150 | openChainsByVM "$vm" 1
1151 | closeChainsByVM "$vm"
1152 | fi
1153 | ;;
1154 | *)
1155 | return 0
1156 | esac
1157 |
1158 | #some debug info
1159 | if [ $DEBUG -eq 0 ] ; then
1160 | logInfo "$vm: $eventName"
1161 | logState
1162 | fi
1163 |
1164 | return 0
1165 | }
1166 |
1167 | #shutdownDaemon [close all]
1168 | #returns: Nothing. Never errors out.
1169 | function shutdownDaemon {
1170 | local closeAll="${1:-1}"
1171 | clearTraps
1172 | PROCESS_EVENTS=1
1173 |
1174 | #make sure the event loop process goes down _before_ the next event
1175 | b_dom0_disconnectEventLoop
1176 |
1177 | if [ $closeAll -eq 0 ] ; then
1178 | logInfo "Received request to close all chains and shut down."
1179 | #NOTE: we force the close as chains may be partially closed and thus be marked as inactive
1180 | closeChains 0 1 "${CHAINS[@]}"
1181 | else
1182 | logInfo "Received request to shut down."
1183 | fi
1184 |
1185 | waitForBackgroundProcesses
1186 | logInfo "$(date) Stopped."
1187 | }
1188 |
1189 | function initTraps {
1190 | trap 'shutdownDaemon 1' EXIT
1191 |
1192 | #ignore Ctrl-C etc. inside the daemon (this overwrites parts of the exit trap above)
1193 | trap '' SIGTERM SIGINT SIGQUIT SIGHUP
1194 |
1195 | trap 'shutdownDaemon 1' SIGUSR1
1196 | trap 'shutdownDaemon 0' SIGUSR2
1197 | }
1198 |
1199 | function clearTraps {
1200 | trap - EXIT
1201 | trap - SIGUSR1
1202 | trap - SIGUSR2
1203 | }
1204 |
1205 | #waitForBackgroundProcesses
1206 | #Wait for all currently running backround operations.
1207 | function waitForBackgroundProcesses {
1208 | local chain=
1209 | local pid=
1210 | for chain in "${CHAINS[@]}" ; do
1211 | pid="${CHAINS2INFO["${chain}_last open pid"]}"
1212 | [ -n "$pid" ] && wait "$pid"
1213 | pid="${CHAINS2INFO["${chain}_last close pid"]}"
1214 | [ -n "$pid" ] && wait "$pid"
1215 | done
1216 | return 0
1217 | }
1218 |
1219 | #daemon main loop
1220 | function daemon_main {
1221 | #
1222 | #service algorithmic sketch:
1223 | #(NOTE: the device attach/detach events do not have the device IDs that we need, which had to be taken into account below)
1224 | #
1225 | # service start:
1226 | # read all config files and create the following maps:
1227 | # map chains2info : chain ids --> all config input, chain open/close command
1228 | # map vms2cains : all VMs --> list of related chain ids
1229 | # enter the Qubes OS event loop
1230 | # init:
1231 | # map V: initialize with all active VMs
1232 | # map C: initialize with all active chain ids
1233 | # attempt to start all chains for which all required VMs are active
1234 | # in the loop:
1235 | # track VMs being started & stopped in map V
1236 | # for every attached device in VM v: openChainsByVM(v,1)
1237 | # for every detached device in VM v: closeChainsByVM(v,1)
1238 | # for every started VM v: openChainsByVM(v,0)
1239 | # for every stopped VM v: closeChainsByVM(v,0)
1240 | # openChainsByVM(v,all=0|1):
1241 | # get the applicable chains (map vms2chains)
1242 | # if all != 0: filter the list of applicable chains to only the ones which are source VMs
1243 | # filter all active chains according to C
1244 | # filter all chains with shut down VMs according to V
1245 | # attempt to open the remaining chains with qcrypt, track the success in C, log & notify on errors
1246 | # closeChainsByVM(v,VM shutdown=0|1):
1247 | # get the applicable chains (map vms2chains)
1248 | # make sure the chains are active according to C
1249 | # if VM shutdown = 0:
1250 | # assume all chains to be "bad"
1251 | # if VM shutdown = 1:
1252 | # test the qcrypt status of all chains
1253 | # partially close all "bad" chains and update C
1254 | # service stop:
1255 | # if [close all] = 0, close all active chains C (indicated to the daemon by different signal)
1256 | # the daemon terminates
1257 |
1258 | b_setErrorHandler "loggingDaemonErrorHandler"
1259 | initTraps
1260 | logInfo "Starting..."
1261 | parseConfigFiles "$CONF_TARGET"
1262 | b_dom0_enterEventLoop "onQubesEvent" 1000
1263 | }
1264 |
1265 | #assertValidTarget
1266 | #Assert that the second argument is a valid target.
1267 | #retuns: nothing, but errors out with [B_E](#B_E) for invalid targets
1268 | #@B_E
1269 | function assertValidTarget {
1270 | local target="$(b_args_get 1)"
1271 | [ -z "$target" ] && target="default"
1272 | getConfigFolder "$target" > /dev/null
1273 | CONF_TARGET="$target"
1274 | }
1275 |
1276 | #initLog [log target]
1277 | #Initialize the given logging destination.
1278 | #returns: Nothing.
1279 | #@B_E
1280 | function initLog {
1281 | local log="$1"
1282 |
1283 | #nothing to do for devices
1284 | [ -f "$log" ] || return 0
1285 |
1286 | #files may require deletion, if too large
1287 | local lc=
1288 | lc="$(b_fs_getLineCount "$log")" || { B_ERR="Failed to obtain the line count of $log. Permission issues?!" ; B_E ; }
1289 | if [ $lc -gt $DEBUG_LINES_MAX ] ; then
1290 | rm -f "$log" || { B_ERR="Failed to remove the file $log." ; B_E ; }
1291 | fi
1292 |
1293 | return 0
1294 | }
1295 |
1296 | b_deps "sort" "date" "qvm-ls" "qubes-prefs" "logger"
1297 | b_args_parse "$@"
1298 | if b_args_getOption "-v" > /dev/null ; then
1299 | DEBUG=0
1300 | initLog "$DEBUG_OUT"
1301 | initLog "$DEBUG_ERR"
1302 | b_daemon_init 1 "" "$DEBUG_OUT" "$DEBUG_ERR"
1303 | else
1304 | DEBUG=1
1305 | b_daemon_init 1
1306 | fi
1307 |
1308 | numArgs="$(b_args_getCount)"
1309 |
1310 | case "$(b_args_get 0)" in
1311 | "start")
1312 | b_args_assertOptions "-v"
1313 | [ $numArgs -gt 2 ] && usage
1314 | assertValidTarget
1315 | b_daemon_start "$DID"
1316 | ;;
1317 | "stop")
1318 | b_args_assertOptions "-v" "-c"
1319 | [ $numArgs -gt 1 ] && usage
1320 | stopC
1321 | ;;
1322 | "restart")
1323 | b_args_assertOptions "-v" "-c"
1324 | [ $numArgs -gt 2 ] && usage
1325 | assertValidTarget
1326 | stopC
1327 | b_daemon_start "$DID"
1328 | ;;
1329 | "check")
1330 | b_args_assertOptions "-v"
1331 | [ $numArgs -gt 2 ] && usage
1332 | assertValidTarget
1333 | checkC
1334 | ;;
1335 | "chains")
1336 | b_args_assertOptions "-v" "-n" "-e"
1337 | [ $numArgs -gt 2 ] && usage
1338 | assertValidTarget
1339 | chainsC
1340 | ;;
1341 | "status")
1342 | b_args_assertOptions "-v"
1343 | [ $numArgs -gt 1 ] && usage
1344 | b_daemon_status "$DID"
1345 | exit $?
1346 | ;;
1347 | *)
1348 | usage
1349 | ;;
1350 | esac
1351 |
--------------------------------------------------------------------------------
/tests/fixtures/1layer01/container:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/3hhh/qcrypt/f9daf5be562abf554bc21d37bfaca445f34bce34/tests/fixtures/1layer01/container
--------------------------------------------------------------------------------
/tests/fixtures/1layer01/keys/target:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/3hhh/qcrypt/f9daf5be562abf554bc21d37bfaca445f34bce34/tests/fixtures/1layer01/keys/target
--------------------------------------------------------------------------------
/tests/fixtures/2layer01/container:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/3hhh/qcrypt/f9daf5be562abf554bc21d37bfaca445f34bce34/tests/fixtures/2layer01/container
--------------------------------------------------------------------------------
/tests/fixtures/2layer01/keys/middle:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/3hhh/qcrypt/f9daf5be562abf554bc21d37bfaca445f34bce34/tests/fixtures/2layer01/keys/middle
--------------------------------------------------------------------------------
/tests/fixtures/2layer01/keys/target:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/3hhh/qcrypt/f9daf5be562abf554bc21d37bfaca445f34bce34/tests/fixtures/2layer01/keys/target
--------------------------------------------------------------------------------
/tests/fixtures/loopdev/NOTE.txt:
--------------------------------------------------------------------------------
1 |
2 | Essentially this is just the 1layer01 container inside a loop file system at [loop dev mount point]/test-folder/container.
3 |
--------------------------------------------------------------------------------
/tests/fixtures/loopdev/keys/target:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/3hhh/qcrypt/f9daf5be562abf554bc21d37bfaca445f34bce34/tests/fixtures/loopdev/keys/target
--------------------------------------------------------------------------------
/tests/fixtures/loopdev/loopfile:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/3hhh/qcrypt/f9daf5be562abf554bc21d37bfaca445f34bce34/tests/fixtures/loopdev/loopfile
--------------------------------------------------------------------------------
/tests/qcrypt.bats:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bats
2 | #
3 | #+Bats tests for qcrypt.
4 | #+
5 | #+Copyright (C) 2021 David Hobach GPLv3
6 | #+0.7
7 |
8 | load "test_common"
9 |
10 | #size of the containers to test luksInit with in MB
11 | QCRYPT_CSIZE="150" #luks1 requires > 2MB, luks2 > 16 MB (they reserve that much metadata) _per_ layer (i.e. 2x for our tests), btrfs > 110 MB
12 | QCRYPT_CSIZE_BYTES="$(( $QCRYPT_CSIZE * 1024 * 1024 ))"
13 |
14 | function setup {
15 | setupQcryptTesting
16 | }
17 |
18 | @test "usage" {
19 | runSL "$QCRYPT"
20 | [ $status -ne 0 ]
21 | [[ "$output" == *"Usage: qcrypt"* ]]
22 | [[ "$output" == *"open"* ]]
23 | [[ "$output" == *"status"* ]]
24 | [[ "$output" == *"luksInit"* ]]
25 | [[ "$output" == *"close"* ]]
26 | [[ "$output" == *"help"* ]]
27 |
28 | #some with incorrect parameters
29 | runSL "$QCRYPT" open --rincorrect -- "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/pstest" "tstkey-ps" "${TEST_STATE["QCRYPT_VM_2"]}"
30 | echo "$output"
31 | [ $status -ne 0 ]
32 | [ -n "$output" ]
33 |
34 | runSL "$QCRYPT" invalidCmd
35 | [ $status -ne 0 ]
36 | [[ "$output" == *"Usage: "* ]]
37 |
38 | runSL "$QCRYPT" close --
39 | [ $status -ne 0 ]
40 | [ -n "$output" ]
41 | }
42 |
43 | #postInitChecks [working dir] [key backup dir] [key size] [expected md5 sums] [src vm] [src file] [key] [dst vm 1] ... [dst vm n]
44 | #[key backup dir]: Must _only_ contain keys that are related to the last init. Otherwise this check may fail.
45 | #[key size]: optional
46 | #[expected md5 sums]: optional
47 | function postInitChecks {
48 | local wd="$1"
49 | local bak="$2"
50 | local keySize="${3:-100}"
51 | local eMd5s="$4"
52 | local src="$5"
53 | local srcFile="$6"
54 | local key="$7"
55 | shift 7
56 |
57 | #compute the key md5s in dom0
58 | local keyMd5s="$(md5sum "$bak"/* | cut -f1 -d' ')"
59 | if [ -n "$eMd5s" ] ; then
60 | [[ "$keyMd5s" == "$eMd5s" ]]
61 | fi
62 |
63 | #ensure working dir is empty
64 | [ -d "$wd" ]
65 | runSL ls -1 "$wd"
66 | [ $status -eq 0 ]
67 | [ -z "$output" ]
68 |
69 | #ensure the source VM received the source container in the right place and of the right size
70 | local srcFileEsc=
71 | printf -v srcFileEsc '%q' "$srcFile"
72 | runSL b_dom0_qvmRun "$src" "stat -c %s $srcFileEsc"
73 | [ $status -eq 0 ]
74 | [[ "$output" == "$QCRYPT_CSIZE_BYTES" ]]
75 |
76 | #check backed up key size & #keys
77 | local bfile=
78 | local cnt=0
79 | for bfile in "$bak"/* ; do
80 | [[ "$bfile" == *"*" ]] && continue
81 | [ -z "$bfile" ] && continue
82 | [ -f "$bfile" ]
83 | cnt=$(( $cnt +1 ))
84 | local bfSize="$(stat -c %s "$bfile")"
85 | [ $bfSize -eq $keySize ]
86 | done
87 | [ $cnt -eq $# ]
88 |
89 | #check md5s in VMs to match the ones in dom0
90 | local vm=
91 | for vm in "$@" ; do
92 | local keyFolder="$(getQcryptKeyFolder "$vm")"
93 | runSL b_dom0_qvmRun "$vm" "md5sum $keyFolder/$key | cut -f1 -d' '"
94 | echo "ref MD5s:"
95 | echo "$keyMd5s"
96 | echo "$vm MD5s:"
97 | echo "$output"
98 | [ $status -eq 0 ]
99 | [ -n "$output" ]
100 |
101 | runSL b_listContains "$keyMd5s" "$output"
102 | [ $status -eq 0 ]
103 | [ -z "$output" ]
104 | done
105 |
106 | return 0
107 | }
108 |
109 | @test "luksInit (have a coffee...)" {
110 | skipIfNotRealRoot
111 |
112 | #failing tests
113 |
114 | #nonexisting VMs
115 | runSL "$QCRYPT" "luksInit" -- "nonex-src" "/tmp/foobar" "tstkey" "dst1" "dst2"
116 | echo "$output"
117 | [ $status -ne 0 ]
118 | [[ "$output" == *"ERROR"* ]]
119 |
120 | runSL "$QCRYPT" "luksInit" -- "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/foobar" "tstkey" "nonexisting-dst1"
121 | echo "$output"
122 | [ $status -ne 0 ]
123 | [[ "$output" == *"ERROR"* ]]
124 |
125 | #existing VMs, not overwriting files
126 | runSL "$QCRYPT" "luksInit" -- "${TEST_STATE["QCRYPT_VM_1"]}" "/etc/hosts" "host-test-key" "${TEST_STATE["QCRYPT_VM_2"]}"
127 | echo "$output"
128 | [ $status -ne 0 ]
129 | [[ "$output" == *"ERROR"* ]]
130 |
131 | #shut down VMs
132 | qvm-shutdown --wait "$UTD_QUBES_TESTVM"
133 | runSL b_dom0_isRunning "$UTD_QUBES_TESTVM"
134 | [ $status -ne 0 ]
135 |
136 | runSL "$QCRYPT" "luksInit" -- "$UTD_QUBES_TESTVM" "/tmp/rand" "tstkey" "${TEST_STATE["QCRYPT_VM_2"]}"
137 | echo "$output"
138 | [ $status -ne 0 ]
139 | [[ "$output" == *"ERROR"* ]]
140 |
141 | #make sure the above didn't start anything
142 | sleep 1
143 | runSL b_dom0_isRunning "$UTD_QUBES_TESTVM"
144 | [ $status -ne 0 ]
145 |
146 | runSL "$QCRYPT" "luksInit" -- "${TEST_STATE["QCRYPT_VM_2"]}" "/tmp/rand" "tstkey" "$UTD_QUBES_TESTVM"
147 | echo "$output"
148 | [ $status -ne 0 ]
149 | [[ "$output" == *"ERROR"* ]]
150 |
151 | #make sure the above didn't start anything
152 | sleep 1
153 | runSL b_dom0_isRunning "$UTD_QUBES_TESTVM"
154 | [ $status -ne 0 ]
155 |
156 | #(mostly) succeeding tests
157 |
158 | #NOTE: dm-crypt requires at least ~5M containers
159 | local wd="$(mktemp -d)"
160 | local bak="$(mktemp -d)"
161 | local s="${QCRYPT_CSIZE}M"
162 | runSL "$QCRYPT" "luksInit" --size "$s" --wd "$wd" --bak "$bak" -- "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/foobar" "tstkey" "${TEST_STATE["QCRYPT_VM_2"]}"
163 | echo "$output"
164 | [ $status -eq 0 ]
165 | [ -n "$output" ]
166 | [[ "$output" != *"ERROR"* ]]
167 | local keyMd5="$(md5sum "$bak"/* | cut -f1 -d ' ')"
168 | postInitChecks "$wd" "$bak" "" "" "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/foobar" "tstkey" "${TEST_STATE["QCRYPT_VM_2"]}"
169 |
170 | #key overwrite shouldn't happen
171 | #NOTE: container overwrite was checked above already
172 | runSL "$QCRYPT" "luksInit" --size "$s" --wd "$wd" --bak "$bak" -- "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/foobar2" "tstkey" "${TEST_STATE["QCRYPT_VM_2"]}"
173 | echo "$output"
174 | [ $status -ne 0 ]
175 | [[ "$output" == *"ERROR"* ]]
176 | #make sure that nothing changed with the old directories
177 | postInitChecks "$wd" "$bak" "" "$keyMd5" "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/foobar" "tstkey" "${TEST_STATE["QCRYPT_VM_2"]}"
178 |
179 | #try again with empty bak
180 | local bak2="$(mktemp -d)"
181 | runSL "$QCRYPT" "luksInit" --size "$s" --wd "$wd" --bak "$bak2" -- "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/foobar3" "tstkey" "${TEST_STATE["QCRYPT_VM_2"]}"
182 | echo "$output"
183 | [ $status -ne 0 ]
184 | [[ "$output" == *"ERROR"* ]]
185 | #make sure that nothing changed with the old directories
186 | postInitChecks "$wd" "$bak" "" "$keyMd5" "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/foobar" "tstkey" "${TEST_STATE["QCRYPT_VM_2"]}"
187 |
188 | runSL ls -1 "$bak2"
189 | [ $status -eq 0 ]
190 | [ -z "$output" ]
191 |
192 | #autostart
193 | runSL b_dom0_isRunning "$UTD_QUBES_TESTVM"
194 | [ $status -ne 0 ]
195 |
196 | rm -f "$bak"/*
197 | runSL "$QCRYPT" "luksInit" --size "$s" --wd "$wd" -a --bak "$bak" -- "$UTD_QUBES_TESTVM" "/tmp/foobar" "tstkey2" "${TEST_STATE["QCRYPT_VM_1"]}"
198 | echo "$output"
199 | [ $status -eq 0 ]
200 | [ -n "$output" ]
201 | [[ "$output" != *"ERROR"* ]]
202 | postInitChecks "$wd" "$bak" "" "" "$UTD_QUBES_TESTVM" "/tmp/foobar" "tstkey2" "${TEST_STATE["QCRYPT_VM_1"]}"
203 |
204 | runSL b_dom0_isRunning "$UTD_QUBES_TESTVM"
205 | [ $status -eq 0 ]
206 |
207 | #different key size, cryptsetup parameter & 2 layers
208 | rm -f "$bak"/*
209 | runSL "$QCRYPT" "luksInit" --cy "${TEST_STATE["QCRYPT_VM_2"]}" '--hash sha256' --cy "$UTD_QUBES_TESTVM" '--hash sha256' --size "$s" --ks 70 --wd "$wd" --bak "$bak" -- "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/kstest" "tstkey-ks" "${TEST_STATE["QCRYPT_VM_2"]}" "$UTD_QUBES_TESTVM"
210 | echo "$output"
211 | [ $status -eq 0 ]
212 | [ -n "$output" ]
213 | [[ "$output" != *"ERROR"* ]]
214 | postInitChecks "$wd" "$bak" 70 "" "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/kstest" "tstkey-ks" "${TEST_STATE["QCRYPT_VM_2"]}" "$UTD_QUBES_TESTVM"
215 |
216 | #test open & close for the one we just initiated
217 | echo "open & close checks for the container that was just created:"
218 | runSL "$QCRYPT" open --mp "/mnt" -- "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/kstest" "tstkey-ks" "${TEST_STATE["QCRYPT_VM_2"]}" "$UTD_QUBES_TESTVM"
219 | echo "$output"
220 | [ $status -eq 0 ]
221 | [[ "$output" == *"Open done."* ]]
222 | [[ "$output" != *"ERROR"* ]]
223 | postOpenChecks 1 1 "/mnt" "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/kstest" "tstkey-ks" "${TEST_STATE["QCRYPT_VM_2"]}" "$UTD_QUBES_TESTVM"
224 |
225 | #test close
226 | runSL "$QCRYPT" close -- "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/kstest" "tstkey-ks" "${TEST_STATE["QCRYPT_VM_2"]}" "$UTD_QUBES_TESTVM"
227 | echo "$output"
228 | [ $status -eq 0 ]
229 | [[ "$output" == *"Close done."* ]]
230 | [[ "$output" != *"ERROR"* ]]
231 | postCloseChecks 0 "/mnt" "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/kstest" "tstkey-ks" "${TEST_STATE["QCRYPT_VM_2"]}" "$UTD_QUBES_TESTVM"
232 |
233 | #reopen to check whether the file we created after the first open is still there
234 | runSL "$QCRYPT" open --mp "/mnt" -- "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/kstest" "tstkey-ks" "${TEST_STATE["QCRYPT_VM_2"]}" "$UTD_QUBES_TESTVM"
235 | echo "$output"
236 | [ $status -eq 0 ]
237 | [[ "$output" == *"Open done."* ]]
238 | [[ "$output" != *"ERROR"* ]]
239 | postOpenChecks 1 0 "/mnt" "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/kstest" "tstkey-ks" "${TEST_STATE["QCRYPT_VM_2"]}" "$UTD_QUBES_TESTVM"
240 |
241 | runSL "$QCRYPT" close -- "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/kstest" "tstkey-ks" "${TEST_STATE["QCRYPT_VM_2"]}" "$UTD_QUBES_TESTVM"
242 | echo "$output"
243 | [ $status -eq 0 ]
244 | [[ "$output" == *"Close done."* ]]
245 | [[ "$output" != *"ERROR"* ]]
246 | postCloseChecks 0 "/mnt" "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/kstest" "tstkey-ks" "${TEST_STATE["QCRYPT_VM_2"]}" "$UTD_QUBES_TESTVM"
247 |
248 | #luksInit with a key store
249 | rm -f "$bak"/*
250 | store="$(mktemp -d)"
251 | local pass="$TEST_PASSWORD"
252 | local kid="my-testkey-id"
253 | #NOTE: we pass the password twice: one for creation, one for opening
254 | echo "$pass"$'\n'"$pass" | {
255 | runSL "$QCRYPT" --size "$s" --keystore "$store" --wd "$wd" --bak "$bak" luksInit "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/keystore-test" "$kid" "$UTD_QUBES_TESTVM"
256 | echo "$output"
257 | [ $status -eq 0 ]
258 | [ -n "$output" ]
259 | [[ "$output" != *"ERROR"* ]]
260 | }
261 | [ -f "$store/keys.lks" ]
262 | postInitChecks "$wd" "$bak" 100 "" "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/keystore-test" "$kid" "$UTD_QUBES_TESTVM"
263 |
264 | #open with keystore
265 | echo "open & close checks with a key store:"
266 | #delete copied key
267 | local user=
268 | user="$(qvm-prefs "$UTD_QUBES_TESTVM" default_user)"
269 | local path=
270 | printf -v path '%q' "/home/$user/.qcrypt/keys/$kid"
271 | runSL b_dom0_execStrIn "$UTD_QUBES_TESTVM" "rm -f $path"
272 | [ $status -eq 0 ]
273 | [ -z "$output" ]
274 |
275 | #make sure the key is gone
276 | runSL "$QCRYPT" status "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/keystore-test" "$kid" "$UTD_QUBES_TESTVM"
277 | echo "$output"
278 | [ $status -ne 0 ]
279 | local re='key available:[ ]+no'
280 | [[ "$output" =~ $re ]]
281 |
282 | runSL "$QCRYPT" --inj "$UTD_QUBES_TESTVM" "keystore:/$store" open "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/keystore-test" "$kid" "$UTD_QUBES_TESTVM"
283 | echo "$output"
284 | [ $status -eq 0 ]
285 | [[ "$output" == *"Open done."* ]]
286 | [[ "$output" != *"ERROR"* ]]
287 | postOpenChecks 1 1 "" "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/keystore-test" "$kid" "$UTD_QUBES_TESTVM"
288 |
289 | runSL "$QCRYPT" close "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/keystore-test" "$kid" "$UTD_QUBES_TESTVM"
290 | echo "$output"
291 | [ $status -eq 0 ]
292 | [[ "$output" == *"Close done."* ]]
293 | [[ "$output" != *"ERROR"* ]]
294 | postCloseChecks 0 "" "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/keystore-test" "$kid" "$UTD_QUBES_TESTVM"
295 |
296 | #cleanup
297 | b_keys_close "$store"
298 | rm -rf "$bak" "$bak2" "$wd" "$store"
299 | }
300 |
301 | #runForceClose [source VM] [source file] [key id] [destination vm 1] ... [destination vm n]
302 | #Runs a qcrypt --force close for the given chain and takes care of the test VM management as the destination VMs are shut down during the process.
303 | function runForceClose {
304 | runSL "$QCRYPT" close --force -- "$@"
305 | echo "$output"
306 | [ $status -eq 0 ]
307 | [[ "$output" != *"ERROR"* ]]
308 | [[ "$output" == *"Close done."* ]]
309 |
310 | shift 3
311 | local vm=
312 | for vm in "$@" ; do
313 | runSL b_dom0_isHalted "$vm"
314 | [ $status -eq 0 ]
315 | [ -z "$output" ]
316 | if [[ "$vm" == "$UTD_QUBES_TESTVM" ]] ; then
317 | runSL b_dom0_ensureRunning "$UTD_QUBES_TESTVM"
318 | echo "$output"
319 | [ $status -eq 0 ]
320 | [ -z "$output" ]
321 | fi
322 | done
323 |
324 | recreateTestVMsIfNeeded
325 | }
326 |
327 |
328 | function singleLayerOpen {
329 | local eOut="$1"
330 |
331 | runSL "$QCRYPT" --cy "${TEST_STATE["QCRYPT_VM_2"]}" '--type luks' open --inj "${TEST_STATE["QCRYPT_VM_2"]}" "$fixturePath/keys/target" --mp "/mnt" -- "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/1layer01" "1layer01" "${TEST_STATE["QCRYPT_VM_2"]}"
332 | echo "$output"
333 | [ $status -eq 0 ]
334 | [[ "$output" == *"$eOut"* ]]
335 | [[ "$output" != *"ERROR"* ]]
336 | postOpenChecks 1 0 "/mnt" "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/1layer01" "1layer01" "${TEST_STATE["QCRYPT_VM_2"]}"
337 | }
338 |
339 | @test "open (have another coffee...)" {
340 | #NOTE: luksInit didn't necessarily run, if we're not root --> we cannot depend on it (that's also why open & close are tested there as well)
341 | local fixturePath="$(getFixturePath "1layer01")"
342 |
343 | #non-existing source VM
344 | runSL "$QCRYPT" open --mp "/mnt" -- "nonexisting-src" "/tmp/1layer01" "1layer01" "${TEST_STATE["QCRYPT_VM_2"]}"
345 | echo "$output"
346 | [ $status -ne 0 ]
347 | [[ "$output" == *"ERROR"* ]]
348 |
349 | #missing key
350 | copyFixture "1layer01" "${TEST_STATE["QCRYPT_VM_1"]}"
351 | runSL "$QCRYPT" open --mp "/mnt" -- "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/1layer01" "1layer01" "${TEST_STATE["QCRYPT_VM_2"]}"
352 | echo "$output"
353 | [ $status -ne 0 ]
354 | [[ "$output" == *"ERROR"* ]]
355 |
356 | #currently the missing key test will at least try to open, i.e. we need to close the now partially open chain
357 | runForceClose "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/1layer01" "1layer01" "${TEST_STATE["QCRYPT_VM_2"]}"
358 |
359 | #missing source container
360 | runSL "$QCRYPT" open --inj "${TEST_STATE["QCRYPT_VM_2"]}" "$fixturePath/keys/target" --mp "/mnt" -- "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/nonexisting" "1layer01" "${TEST_STATE["QCRYPT_VM_2"]}"
361 | echo "$output"
362 | [ $status -ne 0 ]
363 | [[ "$output" == *"ERROR"* ]]
364 |
365 | #incorrect target
366 | runSL "$QCRYPT" open --inj "${TEST_STATE["QCRYPT_VM_2"]}" "$fixturePath/keys/target" --mp "/mnt" -- "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/1layer01" "1layer01" "nonexisting-target"
367 | echo "$output"
368 | [ $status -ne 0 ]
369 | [[ "$output" == *"ERROR"* ]]
370 |
371 | #correct open with a single layer
372 | singleLayerOpen "Open done."
373 |
374 | #doing the same again shouldn't change anything
375 | singleLayerOpen "The chain is already open."
376 |
377 | #correct open with two layers, r/o and call syntax as in help
378 | copyFixture "2layer01" "${TEST_STATE["QCRYPT_VM_1"]}"
379 | local fixturePath="$(getFixturePath "2layer01")"
380 | runSL "$QCRYPT" -a --ro --inj "${TEST_STATE["QCRYPT_VM_2"]}" "$fixturePath/keys/middle" --inj "$UTD_QUBES_TESTVM" "$fixturePath/keys/target" --mp "/mnt" open "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/2layer01" "2layer01" "${TEST_STATE["QCRYPT_VM_2"]}" "$UTD_QUBES_TESTVM"
381 | echo "$output"
382 | [ $status -eq 0 ]
383 | [[ "$output" == *"Open done."* ]]
384 | [[ "$output" != *"ERROR"* ]]
385 | postOpenChecks 0 0 "/mnt" "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/2layer01" "2layer01" "${TEST_STATE["QCRYPT_VM_2"]}" "$UTD_QUBES_TESTVM"
386 | }
387 |
388 | function testStatusAll {
389 | runSL "$QCRYPT" status
390 | echo "$output"
391 | [ $status -eq 0 ]
392 | [[ "$output" != *"ERROR"* ]]
393 |
394 | local re=
395 | re="${TEST_STATE["QCRYPT_VM_1"]}\:loop0 \-\-> ${TEST_STATE["QCRYPT_VM_2"]}\:[a-z0-9\-]+ \(r/w\)"
396 | echo "$re"
397 | [[ "$output" =~ $re ]]
398 | re="qcrypt status -- ${TEST_STATE["QCRYPT_VM_1"]} /tmp/1layer01 (UNKNOWN-KEY|1layer01) ${TEST_STATE["QCRYPT_VM_2"]}"
399 | echo "$re"
400 | [[ "$output" =~ $re ]]
401 | re="${TEST_STATE["QCRYPT_VM_1"]}\:loop1 \-\-> ${TEST_STATE["QCRYPT_VM_2"]}\:dm-1 --> ${UTD_QUBES_TESTVM}\:xvdi \(r/o\)"
402 | echo "$re"
403 | [[ "$output" =~ $re ]]
404 | re="qcrypt status -- ${TEST_STATE["QCRYPT_VM_1"]} /tmp/2layer01 2layer01 ${TEST_STATE["QCRYPT_VM_2"]} ${UTD_QUBES_TESTVM}"
405 | echo "$re"
406 | [[ "$output" =~ $re ]]
407 | }
408 |
409 | @test "status - all" {
410 | testStatusAll
411 |
412 | #Qubes OS qvm-block ls has a slightly different output when the target device is not mounted --> we need to test that as well
413 | #update: apparently not (unsure as to why it sometimes has the last hop and sometimes not), but we can check for the future ^^
414 | runSL b_dom0_qvmRun "$UTD_QUBES_TESTVM" "umount /mnt"
415 | [ $status -eq 0 ]
416 | [ -z "$output" ]
417 |
418 | testStatusAll
419 |
420 | #cleanup
421 | runSL b_dom0_qvmRun "$UTD_QUBES_TESTVM" "mount /dev/mapper/2layer01 /mnt"
422 | echo "$output"
423 | [ $status -eq 0 ]
424 | }
425 |
426 | #testSuccAllStatus [# of dst VMs] [status parameters]
427 | #only use this if the device is also supposed to be mounted
428 | function testSuccAllStatus {
429 | local re=
430 | local dstCnt="$1"
431 | shift
432 |
433 | runSL "$QCRYPT" status "$@"
434 | echo "$output"
435 | [ $status -eq 0 ]
436 | [[ "$output" != *"no"* ]]
437 | [[ "$output" == *"yes"* ]]
438 | re='device mounted:[[:space:]]+yes'
439 | [[ "$output" =~ $re ]]
440 | re='source:[[:space:]]+file, loop device'
441 | [[ "$output" =~ $re ]]
442 |
443 | #count all "yes"
444 | local yesCnt="$(echo "$output" | grep -E '\s\syes\s' | wc -l)"
445 | local maxCnt=$(( $dstCnt * 4 +2 ))
446 | echo "yesCnt = $yesCnt"
447 | echo "maxCnt = $maxCnt"
448 | [ $yesCnt -eq $maxCnt ]
449 |
450 | return 0
451 | }
452 |
453 | #testFailStatus [expected status] [# of dst VMs] [status parameters]
454 | function testFailStatus {
455 | local eStatus="$1"
456 | local dstCnt="$2"
457 | shift 2
458 | runSL "$QCRYPT" status "$@"
459 | echo "$output"
460 | [ $status -ne 0 ]
461 |
462 | if [ -n "$eStatus" ] ; then
463 | [ $status -eq $eStatus ]
464 | fi
465 |
466 | [[ "$output" == *"no"* ]] || [[ "$output" == *"ERROR"* ]] || [[ "$output" == *"Usage: qcrypt"* ]]
467 |
468 | if [[ "$output" != *"ERROR"* ]] && [[ "$output" != *"Usage"* ]] ; then
469 | local yesCnt="$(echo "$output" | grep -E '\s\s+yes\s+' | wc -l)"
470 | local maxCnt=$(( $dstCnt * 4 +2 ))
471 | local noCnt="$(echo "$output" | grep -E '\s\s+no\s+' | wc -l)"
472 | echo "yesCnt = $yesCnt"
473 | echo "noCnt = $noCnt"
474 | echo "maxCnt = $maxCnt"
475 | echo "status = $status"
476 |
477 | [ $yesCnt -ge 0 ]
478 | [ $yesCnt -lt $maxCnt ]
479 |
480 | [ $noCnt -gt 0 ]
481 | [ $(( $yesCnt + $noCnt )) -eq $maxCnt ]
482 |
483 | #the "device mounted: no" may be ok, if --mp was not specified
484 | local minStat=$(( $noCnt -1 ))
485 | [ $status -ge $minStat ]
486 | fi
487 |
488 | return 0
489 | }
490 |
491 | @test "status - single" {
492 | #correct status for the two open chains
493 | testSuccAllStatus 1 --mp "/mnt" -- "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/1layer01" "1layer01" "${TEST_STATE["QCRYPT_VM_2"]}"
494 | testSuccAllStatus 2 --mp "/mnt" -- "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/2layer01" "2layer01" "${TEST_STATE["QCRYPT_VM_2"]}" "$UTD_QUBES_TESTVM"
495 | testSuccAllStatus 1 -- "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/1layer01" "1layer01" "${TEST_STATE["QCRYPT_VM_2"]}"
496 | testSuccAllStatus 2 -- "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/2layer01" "2layer01" "${TEST_STATE["QCRYPT_VM_2"]}" "$UTD_QUBES_TESTVM"
497 |
498 | #status should also work with // somewhere (happens quite often in programs)
499 | testSuccAllStatus 1 -- "${TEST_STATE["QCRYPT_VM_1"]}" "//tmp/1layer01" "1layer01" "${TEST_STATE["QCRYPT_VM_2"]}"
500 | testSuccAllStatus 1 -- "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp//1layer01" "1layer01" "${TEST_STATE["QCRYPT_VM_2"]}"
501 | testSuccAllStatus 1 -- "${TEST_STATE["QCRYPT_VM_1"]}" "//tmp//1layer01" "1layer01" "${TEST_STATE["QCRYPT_VM_2"]}"
502 |
503 |
504 | #status for invalid & incomplete chains
505 | testFailStatus "" 1 -- "nonexisting-src" "/tmp/1layer01" "1layer01" "${TEST_STATE["QCRYPT_VM_2"]}"
506 | testFailStatus "" 1 -- "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/1layer01" "1layer01" "nonexisting-dst"
507 | testFailStatus "" 1 -- "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/1layer01" "invalid-key" "${TEST_STATE["QCRYPT_VM_2"]}"
508 | testFailStatus "" 1 -- "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/nonexisting" "1layer01" "${TEST_STATE["QCRYPT_VM_2"]}"
509 | testFailStatus "" 1 -- "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/1layer01" "1layer01"
510 | testFailStatus "" 2 -- "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/2layer01" "nonexisting-key" "${TEST_STATE["QCRYPT_VM_2"]}" "$UTD_QUBES_TESTVM"
511 | testFailStatus "" 1 -- "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/2layer01" "2layer01" "${TEST_STATE["QCRYPT_VM_2"]}"
512 | testFailStatus "" 1 -- "${TEST_STATE["QCRYPT_VM_2"]}" "/tmp/2layer01" "2layer01" "$UTD_QUBES_TESTVM"
513 | testFailStatus "" 2 -- "/tmp/2layer01" "2layer01" "${TEST_STATE["QCRYPT_VM_1"]}" "${TEST_STATE["QCRYPT_VM_2"]}" "$UTD_QUBES_TESTVM"
514 | testFailStatus "" 2 -- "${TEST_STATE["QCRYPT_VM_2"]}" "/tmp/2layer01" "2layer01" "${TEST_STATE["QCRYPT_VM_1"]}" "$UTD_QUBES_TESTVM"
515 | testFailStatus "" 3 -- "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/2layer01" "2layer01" "${TEST_STATE["QCRYPT_VM_2"]}" "incorr-vm" "$UTD_QUBES_TESTVM"
516 | testFailStatus "" 3 -- "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/2layer01" "2layer01" "${TEST_STATE["QCRYPT_VM_2"]}" "$UTD_QUBES_TESTVM" "incorr-vm"
517 |
518 | #partially close & check status (the close test below should work with partial closes anyway)
519 | runSL b_dom0_qvmRun "$UTD_QUBES_TESTVM" "umount /mnt"
520 | [ $status -eq 0 ]
521 | [ -z "$output" ]
522 | #NOTE: without --mp specified, an unmounted end device should not cause a non-zero exit code, with --mp (mount checking), it should
523 | runSL "$QCRYPT" status -- "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/2layer01" "2layer01" "${TEST_STATE["QCRYPT_VM_2"]}" "$UTD_QUBES_TESTVM"
524 | [ $status -eq 0 ]
525 | local re='device mounted:[ ]+no'
526 | [[ "$output" =~ $re ]]
527 | [[ "$output" != *"ERROR"* ]]
528 | testFailStatus 1 2 --mp "" -- "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/2layer01" "2layer01" "${TEST_STATE["QCRYPT_VM_2"]}" "$UTD_QUBES_TESTVM"
529 | runSL b_dom0_qvmRun "$UTD_QUBES_TESTVM" "cryptsetup close /dev/mapper/2layer01"
530 | [ $status -eq 0 ]
531 | [ -z "$output" ]
532 | testFailStatus 2 2 --mp "" -- "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/2layer01" "2layer01" "${TEST_STATE["QCRYPT_VM_2"]}" "$UTD_QUBES_TESTVM"
533 | }
534 |
535 | @test "close" {
536 | #invalid options
537 | runSL "$QCRYPT" close -invalid -- "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/1layer01" "1layer01" "${TEST_STATE["QCRYPT_VM_2"]}"
538 | echo "$output"
539 | [ $status -ne 0 ]
540 | [[ "$output" != *"Close done."* ]]
541 |
542 | #wrong src VM
543 | runSL "$QCRYPT" close -- "$UTD_QUBES_TESTVM" "/tmp/1layer01" "1layer01" "${TEST_STATE["QCRYPT_VM_2"]}"
544 | echo "$output"
545 | [ $status -ne 0 ]
546 | [[ "$output" == *"ERROR"* ]]
547 |
548 | #wrong target VM
549 | runSL "$QCRYPT" close -- "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/1layer01" "1layer01" "$UTD_QUBES_TESTVM"
550 | echo "$output"
551 | [ $status -ne 0 ]
552 | [[ "$output" == *"ERROR"* ]]
553 |
554 | #wrong key
555 | runSL "$QCRYPT" close -- "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/1layer01" "invalidkey" "${TEST_STATE["QCRYPT_VM_2"]}"
556 | echo "$output"
557 | [ $status -ne 0 ]
558 | [[ "$output" == *"ERROR"* ]]
559 |
560 | #wrong source file
561 | runSL "$QCRYPT" close -- "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/foobar" "1layer01" "${TEST_STATE["QCRYPT_VM_2"]}"
562 | echo "$output"
563 | [ $status -ne 0 ]
564 | [[ "$output" == *"ERROR"* ]]
565 |
566 | #make sure the closes above didn't have an effect
567 | postOpenChecks 1 0 "/mnt" "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/1layer01" "1layer01" "${TEST_STATE["QCRYPT_VM_2"]}"
568 |
569 | #successful close with the ones that were opened during the open test
570 | runSL "$QCRYPT" close -- "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/1layer01" "1layer01" "${TEST_STATE["QCRYPT_VM_2"]}"
571 | echo "$output"
572 | [ $status -eq 0 ]
573 | [[ "$output" == *"Close done."* ]]
574 | [[ "$output" != *"ERROR"* ]]
575 | postCloseChecks 0 "/mnt" "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/1layer01" "1layer01" "${TEST_STATE["QCRYPT_VM_2"]}"
576 |
577 | #partial closes shouldn't work
578 | runSL "$QCRYPT" close -- "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/2layer01" "2layer01" "${TEST_STATE["QCRYPT_VM_2"]}"
579 | echo "$output"
580 | [ $status -ne 0 ]
581 | [[ "$output" == *"ERROR"* ]]
582 |
583 | runSL "$QCRYPT" close -- "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/2layer01" "2layer01" "$UTD_QUBES_TESTVM"
584 | echo "$output"
585 | [ $status -ne 0 ]
586 | [[ "$output" == *"ERROR"* ]]
587 |
588 | runSL "$QCRYPT" close -- "${TEST_STATE["QCRYPT_VM_2"]}" "/tmp/2layer01" "2layer01" "$UTD_QUBES_TESTVM"
589 | echo "$output"
590 | [ $status -ne 0 ]
591 | [[ "$output" == *"ERROR"* ]]
592 |
593 | #we need --force because it was partially closed by the status test
594 | runForceClose "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/2layer01" "2layer01" "${TEST_STATE["QCRYPT_VM_2"]}" "$UTD_QUBES_TESTVM"
595 | }
596 |
597 | @test "partial close (VM down)" {
598 | #this deserves an extra test as it is _very_ important for qcryptd
599 | #maybe TODO: test with 2 layers; especially since that tends to trigger Qubes OS bug #4784
600 |
601 | #open, this time VM_2 as source --> VM_1
602 | copyFixture "1layer01" "${TEST_STATE["QCRYPT_VM_2"]}"
603 | runSL "$QCRYPT" open --ro --inj "${TEST_STATE["QCRYPT_VM_1"]}" "$(getFixturePath "1layer01/keys/target")" --mp "/mntpartial" -- "${TEST_STATE["QCRYPT_VM_2"]}" "/tmp/1layer01" "1layer01" "${TEST_STATE["QCRYPT_VM_1"]}"
604 | echo "$output"
605 | [ $status -eq 0 ]
606 | [[ "$output" == *"Open done."* ]]
607 | [[ "$output" != *"ERROR"* ]]
608 | postOpenChecks 1 0 "/mntpartial" "${TEST_STATE["QCRYPT_VM_2"]}" "/tmp/1layer01" "1layer01" "${TEST_STATE["QCRYPT_VM_1"]}"
609 | testSuccAllStatus 1 -- "${TEST_STATE["QCRYPT_VM_2"]}" "/tmp/1layer01" "1layer01" "${TEST_STATE["QCRYPT_VM_1"]}"
610 |
611 | run qvm-shutdown --timeout 10 --wait "${TEST_STATE["QCRYPT_VM_1"]}"
612 | echo "$output"
613 | runSL b_dom0_isRunning "${TEST_STATE["QCRYPT_VM_1"]}"
614 | echo "$output"
615 | [ $status -ne 0 ]
616 |
617 | #status should not just error out
618 | testFailStatus 5 1 --mp "" -- "${TEST_STATE["QCRYPT_VM_2"]}" "/tmp/1layer01" "1layer01" "${TEST_STATE["QCRYPT_VM_1"]}"
619 | [[ "$output" != *"ERROR"* ]]
620 |
621 | #partial close should work
622 | local old1="${TEST_STATE["QCRYPT_VM_1"]}"
623 | local old2="${TEST_STATE["QCRYPT_VM_2"]}"
624 | runForceClose "${TEST_STATE["QCRYPT_VM_2"]}" "/tmp/1layer01" "1layer01" "${TEST_STATE["QCRYPT_VM_1"]}"
625 |
626 | #NOTE: ${TEST_STATE["QCRYPT_VM_1"]} is now another one...
627 | [[ "${TEST_STATE["QCRYPT_VM_1"]}" != "$old1" ]]
628 | [[ "${TEST_STATE["QCRYPT_VM_2"]}" == "$old2" ]]
629 | testFailStatus 5 1 --mp "" -- "${TEST_STATE["QCRYPT_VM_2"]}" "/tmp/1layer01" "1layer01" "${TEST_STATE["QCRYPT_VM_1"]}"
630 | [[ "$output" != *"ERROR"* ]]
631 | }
632 |
633 | @test "cleanup" {
634 | runCleanup
635 | }
636 |
--------------------------------------------------------------------------------
/tests/qcryptd.bats:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bats
2 | #
3 | #+Bats tests for qcryptd. These tests assume that qcrypt is working correctly (qcrypt.bats ran successfully).
4 | #+
5 | #+Copyright (C) 2020 David Hobach GPLv3
6 | #+0.6
7 |
8 | load "test_common"
9 |
10 | #used by the start test & its helper functions
11 | OUT_NONEXISTING=
12 | OUT_DEST_DOWN=
13 | OUT_DEV_MISSING=
14 |
15 | function setup {
16 | setupQcryptTesting
17 | }
18 |
19 | @test "usage" {
20 | runSL "$QCRYPTD"
21 | [ $status -ne 0 ]
22 | [[ "$output" == *"Usage: qcryptd"* ]]
23 | [[ "$output" == *"start"* ]]
24 | [[ "$output" == *"status"* ]]
25 | [[ "$output" == *"stop"* ]]
26 | [[ "$output" == *"restart"* ]]
27 | [[ "$output" == *"check"* ]]
28 | [[ "$output" == *"help"* ]]
29 |
30 | runSL "$QCRYPTD" "help"
31 | [ $status -ne 0 ]
32 | [[ "$output" == *"Usage: qcryptd"* ]]
33 |
34 | runSL "$QCRYPTD" incorrectCmd
35 | [ $status -ne 0 ]
36 | [ -n "$output" ]
37 |
38 | runSL "$QCRYPTD" --qincorrect status
39 | [ $status -ne 0 ]
40 | [ -n "$output" ]
41 |
42 | runSL "$QCRYPTD" --qincorrect start
43 | [ $status -ne 0 ]
44 | [ -n "$output" ]
45 |
46 | runSL "$QCRYPTD" -v --qincorrect stop
47 | [ $status -ne 0 ]
48 | [ -n "$output" ]
49 |
50 | runSL "$QCRYPTD" --qincorrect -v check
51 | [ $status -ne 0 ]
52 | [ -n "$output" ]
53 | }
54 |
55 | @test "check" {
56 | runSL "$QCRYPTD" check "nonexisting"
57 | [ $status -ne 0 ]
58 | [ -n "$output" ]
59 |
60 | #invalid configs
61 | local i=
62 | for ((i=1;i<=6;i++)) ; do
63 | runSL "$QCRYPTD" -v check "test-invalid-0$i"
64 | echo "$output"
65 | [[ "$output" == *"ERROR"* ]]
66 | [[ "$output" != *"All good."* ]]
67 | [ $status -ne 0 ]
68 | done
69 |
70 | #valid configs
71 | local validConfs=("examples" "test-valid-01" "test-valid-02" "test-valid-03")
72 | local conf=
73 | for conf in "${validConfs[@]}" ; do
74 | runSL "$QCRYPTD" -v check "$conf"
75 | echo "$output"
76 | [[ "$output" != *"ERROR"* ]]
77 | [[ "$output" == *"All good."* ]]
78 | [ $status -eq 0 ]
79 | done
80 | }
81 |
82 | @test "config parsing" {
83 | runSL "$QCRYPTD" check -v "test-valid-03"
84 | [[ "$output" != *"ERROR"* ]]
85 | [[ "$output" == *"All good."* ]]
86 | [ $status -eq 0 ]
87 | local decl="$(echo "$output" | grep "declare" | grep -v "VMS2CHAINS")"
88 | echo "$decl"
89 | eval "$decl"
90 |
91 | [[ "${CHAINS[0]}" == "ex01" ]]
92 | [[ "${CHAINS[1]}" == "valid03" ]]
93 | echo 0
94 |
95 | #ex01 chain
96 | local chain="ex01"
97 | [[ "${CHAINS2INFO["${chain}_source vm"]}" == "sys-usb" ]]
98 | [[ "${CHAINS2INFO["${chain}_source device"]}" == "/dev/disk/by-uuid/8c3663c5-b9345-6381-9a67-dd813eb12863" ]]
99 | [[ "${CHAINS2INFO["${chain}_source mount point"]}" == "/mnt-ex01" ]]
100 | [[ "${CHAINS2INFO["${chain}_source file"]}" == "/containers/ex01-container.luks" ]]
101 | echo 1
102 | [[ "${CHAINS2INFO["${chain}_key"]}" == "ex01-key" ]]
103 | [[ "${CHAINS2INFO["${chain}_destination vm 1"]}" == "d-testing" ]]
104 | [[ "${CHAINS2INFO["${chain}_destination inj 1"]}" == "/root/qcrypt-keys/ex01_disp" ]]
105 | [[ "${CHAINS2INFO["${chain}_destination opt 1"]}" == "--type plain --cipher aes-xts-plain64 -s 512 --hash sha512" ]]
106 | [[ "${CHAINS2INFO["${chain}_destination vm 2"]}" == "work" ]]
107 | [[ "${CHAINS2INFO["${chain}_destination inj 2"]}" == "" ]]
108 | [[ "${CHAINS2INFO["${chain}_destination opt 2"]}" == "" ]]
109 | [[ "${CHAINS2INFO["${chain}_destination mount point"]}" == "/qcrypt-ex01" ]]
110 | echo 2
111 | [[ "${CHAINS2INFO["${chain}_autostart"]}" == "1" ]]
112 | [[ "${CHAINS2INFO["${chain}_read-only"]}" == "1" ]]
113 | [[ "${CHAINS2INFO["${chain}_startup interval"]}" == "300" ]]
114 | [[ "${CHAINS2INFO["${chain}_pre open command"]}" == "" ]]
115 | echo 3
116 | [[ "${CHAINS2INFO["${chain}_post open command"]}" == "" ]]
117 | [[ "${CHAINS2INFO["${chain}_pre close command"]}" == "" ]]
118 | [[ "${CHAINS2INFO["${chain}_post close command"]}" == "" ]]
119 | echo a
120 | local mainCmd="sys-usb /mnt-ex01//containers/ex01-container.luks ex01-key d-testing work"
121 | [[ "${CHAINS2INFO["${chain}_open"]}" == *"qcrypt --inj d-testing /root/qcrypt-keys/ex01_disp --cy d-testing '--type plain --cipher aes-xts-plain64 -s 512 --hash sha512' --mp /qcrypt-ex01 open -- $mainCmd" ]]
122 | echo b
123 | [[ "${CHAINS2INFO["${chain}_status"]}" == *"qcrypt status --mp \"\" -- $mainCmd" ]]
124 | [[ "${CHAINS2INFO["${chain}_close"]}" == *"qcrypt close --force -- $mainCmd" ]]
125 | echo c
126 |
127 | #valid03 chain
128 | local chain="valid03"
129 | [[ "${CHAINS2INFO["${chain}_source vm"]}" == "another-usb" ]]
130 | [[ "${CHAINS2INFO["${chain}_source device"]}" == "/dev/disk/by-uuid/8c3663c5-b9345-6381-9a67-dd813eb12864" ]]
131 | [[ "${CHAINS2INFO["${chain}_source mount point"]}" == "/mnt-ex03" ]]
132 | [[ "${CHAINS2INFO["${chain}_source file"]}" == "/containers/ex03-container.luks" ]]
133 | [[ "${CHAINS2INFO["${chain}_key"]}" == "ex03-key" ]]
134 | [[ "${CHAINS2INFO["${chain}_destination vm 1"]}" == "d-testing" ]]
135 | [[ "${CHAINS2INFO["${chain}_destination inj 1"]}" == "/root/qcrypt-keys/ex03_disp" ]]
136 | [[ "${CHAINS2INFO["${chain}_destination vm 2"]}" == "work" ]]
137 | [[ "${CHAINS2INFO["${chain}_destination inj 2"]}" == "/another/path.key" ]]
138 | [[ "${CHAINS2INFO["${chain}_destination vm 3"]}" == "work2" ]]
139 | [[ "${CHAINS2INFO["${chain}_destination inj 3"]}" == "/another/path2.key" ]]
140 | [[ "${CHAINS2INFO["${chain}_destination mount point"]}" == "/qcrypt-ex03" ]]
141 | [[ "${CHAINS2INFO["${chain}_autostart"]}" == "0" ]]
142 | [[ "${CHAINS2INFO["${chain}_read-only"]}" == "0" ]]
143 | [[ "${CHAINS2INFO["${chain}_startup interval"]}" == "5" ]]
144 | [[ "${CHAINS2INFO["${chain}_pre open command"]}" == 'logger "starting the ex03 chain"' ]]
145 | [[ "${CHAINS2INFO["${chain}_post open command"]}" == 'logger "started the ex03 chain"' ]]
146 | [[ "${CHAINS2INFO["${chain}_pre close command"]}" == 'logger "attempting to close the ex03 chain"' ]]
147 | [[ "${CHAINS2INFO["${chain}_post close command"]}" == 'logger "stopped the ex03 chain"' ]]
148 | echo d
149 | local mainCmd="another-usb /mnt-ex03//containers/ex03-container.luks ex03-key d-testing work work2"
150 | [[ "${CHAINS2INFO["${chain}_open"]}" == *"qcrypt --inj d-testing /root/qcrypt-keys/ex03_disp --cy d-testing '--type luks' --inj work /another/path.key --inj work2 /another/path2.key --cy work2 '--type luks' -a --ro --mp /qcrypt-ex03 open -- $mainCmd" ]]
151 | echo e
152 | [[ "${CHAINS2INFO["${chain}_status"]}" == *"qcrypt status --mp \"\" -- $mainCmd" ]]
153 | [[ "${CHAINS2INFO["${chain}_close"]}" == *"qcrypt close --force -- $mainCmd" ]]
154 | echo f
155 | }
156 |
157 | @test "chains" {
158 | runSL "$QCRYPTD" chains -v "nonexisting"
159 | [ $status -ne 0 ]
160 | [ -n "$output" ]
161 |
162 | runSL "$QCRYPTD" chains -v "test-valid-01"
163 | [ $status -eq 0 ]
164 | [[ "$output" == *"qcrypt status"* ]]
165 | [[ "$output" == *"ex01.ini"* ]]
166 | [[ "$output" == *"valid01.ini"* ]]
167 | local cnt="$(echo "$output" | wc -l)"
168 | [ $cnt -eq 2 ]
169 |
170 | runSL "$QCRYPTD" chains -v -n "test-valid-01"
171 | [ $status -eq 0 ]
172 | [[ "$output" == *"qcrypt status"* ]]
173 | [[ "$output" != *"ex01.ini"* ]]
174 | [[ "$output" != *"valid01.ini"* ]]
175 | local cnt="$(echo "$output" | wc -l)"
176 | [ $cnt -eq 2 ]
177 |
178 | runSL "$QCRYPTD" chains -v -e -n "test-valid-01"
179 | [ $status -eq 2 ]
180 | [[ "$output" == *"qcrypt status"* ]]
181 | [[ "$output" == *"state: bad"* ]]
182 | [[ "$output" != *"state: good"* ]]
183 | [[ "$output" != *"ex01.ini"* ]]
184 | [[ "$output" != *"valid01.ini"* ]]
185 | local cnt="$(echo "$output" | wc -l)"
186 | [ $cnt -eq 4 ]
187 | }
188 |
189 | @test "stop (invalid)" {
190 | skipIfQcryptdRunning
191 |
192 | #stop a non-running qcrypt instance
193 | runSL "$QCRYPTD" stop
194 | [ $status -ne 0 ]
195 | [[ "$output" == *"wasn't running"* ]]
196 |
197 | runSL "$QCRYPTD" -fooo stop
198 | [ $status -ne 0 ]
199 | [[ "$output" == *"ERROR"* ]]
200 |
201 | runSL "$QCRYPTD" -c stop
202 | [ $status -ne 0 ]
203 | [[ "$output" == *"wasn't running"* ]]
204 | }
205 |
206 | @test "start (invalid)" {
207 | skipIfQcryptdRunning
208 |
209 | runSL "$QCRYPTD" start "nonexisting-hopefully"
210 | [ $status -ne 0 ]
211 | [[ "$output" == *"ERROR"* ]]
212 |
213 | runSL "$QCRYPTD" --incorrect start
214 | [ $status -ne 0 ]
215 | [[ "$output" == *"ERROR"* ]]
216 | }
217 |
218 | #prepareQcryptdStartTest [target folder]
219 | function prepareQcryptdStartTest {
220 | local targetFolder="$1"
221 | [ -d "$targetFolder" ]
222 |
223 | local fixPath="$(getFixturePath)"
224 | local fix1LayerKey="$fixPath/1layer01/keys/target"
225 | local fix1LayerContainer="$fixPath/1layer01/container"
226 | local fixLoopDevKey="$fixPath/loopdev/keys/target"
227 | local fixLoopDevFile="$fixPath/loopdev/loopfile"
228 |
229 | #available options:
230 | #source vm=required
231 | #source device=/
232 | #source mount point=
233 | #source file=required
234 | #key=required
235 | #destination vm 1 = required
236 | #destination inj 1 = required
237 | #destination vm 2 = work
238 | #destination inj 2 =
239 | #destination mount point=/qcrypt-ex01
240 | #autostart=false
241 | #read-only=false
242 | #type=luks
243 | #pre open command=echo starting
244 | #post open command=echo started
245 | #pre close command=echo closing
246 | #post close command=echo closed
247 |
248 | #non-existing destination VM
249 | echo '
250 | source vm='"$UTD_QUBES_TESTVM"'
251 | source file=/tmp/container
252 | key=nonexisting-key
253 | destination vm 1 = nonexisting-vm
254 | destination inj 1 = '"$fix1LayerKey"'
255 | destination mount point=/mnt-nonexisting
256 | autostart=false
257 | read-only=false
258 | pre open command=echo starting >> '"$OUT_NONEXISTING"'
259 | post open command=echo started >> '"$OUT_NONEXISTING"'
260 | pre close command=echo closing >> '"$OUT_NONEXISTING"'
261 | post close command=echo closed >> '"$OUT_NONEXISTING"'
262 | ' > "$targetFolder/nonexisting.ini"
263 |
264 | #NOTE: the container is not copied (yet) as UTD_QUBES_TESTVM is supposed to be down
265 |
266 | #destination VM down
267 | echo '
268 | source vm='"${TEST_STATE["QCRYPT_VM_1"]}"'
269 | source file=/tmp/container
270 | key=dest-down-key
271 | destination vm 1 = '"$UTD_QUBES_TESTVM"'
272 | destination inj 1 = '"$fix1LayerKey"'
273 | destination mount point=/mnt-dest-down
274 | pre open command=echo starting >> '"$OUT_DEST_DOWN"'
275 | post open command=echo started >> '"$OUT_DEST_DOWN"'
276 | pre close command=echo closing >> '"$OUT_DEST_DOWN"'
277 | post close command=echo closed >> '"$OUT_DEST_DOWN"'
278 | ' > "$targetFolder/dest-down.ini"
279 |
280 | runSL b_dom0_copy "$fix1LayerContainer" "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/" 0
281 | [ $status -eq 0 ]
282 | [ -z "$output" ]
283 |
284 | #source device missing
285 | #NOTE: we use /dev/loop0 in a fresh disposable VM
286 | echo '
287 | source vm='"${TEST_STATE["QCRYPT_VM_2"]}"'
288 | source device=/dev/loop0
289 | source mount point=/srcmnt
290 | source file=/test-folder/container
291 | key=dev-missing-key
292 | read-only=false
293 | destination vm 1 = '"${TEST_STATE["QCRYPT_VM_1"]}"'
294 | destination inj 1 = '"$fixLoopDevKey"'
295 | destination mount point=/mnt-dev-missing
296 | pre open command=echo starting >> '"$OUT_DEV_MISSING"'
297 | post open command=echo started >> '"$OUT_DEV_MISSING"'
298 | pre close command=echo closing >> '"$OUT_DEV_MISSING"'
299 | post close command=echo closed >> '"$OUT_DEV_MISSING"'
300 | ' > "$targetFolder/dev-missing.ini"
301 |
302 | runSL b_dom0_copy "$fixLoopDevFile" "${TEST_STATE["QCRYPT_VM_2"]}" "/tmp/" 0
303 | [ $status -eq 0 ]
304 | [ -z "$output" ]
305 | }
306 |
307 | #assertOutput [output file] [state] [prefix]
308 | #Check whether the given qcrypt pre/post open/close command output matches the expected one.
309 | #[state]: 0=not started, 1=started, 2=started and stopped already, 3=never started, but force closed, 4=started, stopped and force closed
310 | function assertOutput {
311 | local outFile="$1"
312 | local state="$2"
313 | local prefix="$3"
314 |
315 | local out="fail"
316 | out="$(cat "$outFile")"
317 | echo "$out"
318 |
319 | local expected=""
320 | [ "$state" -ge 1 ] && expected="starting"$'\n'"started"
321 | [ "$state" -ge 2 ] && expected="$expected"$'\n'"closing"$'\n'"closed"
322 | [ "$state" -eq 3 ] && expected="closing"$'\n'"closed"
323 | [ "$state" -ge 4 ] && expected="$expected"$'\n'"closing"$'\n'"closed"
324 | expected="${prefix}$expected"
325 | [[ "$out" == "$expected" ]]
326 | }
327 |
328 | #checkTestChains [destination VM down chain state] [source device missing chain state] [nonexisting state] [qcryptd status] [destination VM down output prefix] [source device missing prefix]
329 | #Check the status of the test chains to match the expected one.
330 | #[... chain state]: 0=never started, 1=started (source file missing), 2=started and stopped already, 3=never started, but force closed, 4=started, stopped and force closed
331 | #[nonexisting state]: 0=source not available, 1=source available, 3=after force close
332 | #[qcryptd status]: expected exit code of qcryptd status (default: 0)
333 | function checkTestChains {
334 | local chainDownState="$1"
335 | local chainMissingState="$2"
336 | local nonexistingState="${3:-0}"
337 | local qcryptdStatus="${4:-0}"
338 | local chainDownPrefix="$5"
339 | local chainMissingPrefix="$6"
340 |
341 | #postCloseChecks [mount point] [source vm] [source device] [key id] [destination vm 1] .. [destination vm n]
342 | #make sure the service is running
343 | runSL "$QCRYPTD" status
344 | [ $status -eq $qcryptdStatus ]
345 | [ -n "$output" ]
346 | [[ "$output" != *"ERROR"* ]]
347 |
348 | #nonextising VM chain
349 | echo a
350 | local mod=0
351 | if [ $nonexistingState -ne 1 ] ; then
352 | #if the source VM is running, we need to account for the missing source file
353 | qvm-check --running "$UTD_QUBES_TESTVM" &> /dev/null && mod=1
354 | fi
355 | postCloseChecks "$mod" "/mnt-nonexisting" "$UTD_QUBES_TESTVM" "/tmp/container" "nonexisting-key" "nonexisting-vm"
356 | echo b
357 | [ $nonexistingState -eq 3 ] && assertOutput "$OUT_NONEXISTING" 3 || assertOutput "$OUT_NONEXISTING" 0
358 |
359 | #destination VM down chain
360 | echo c
361 | if [ "$chainDownState" -eq 1 ] ; then
362 | postOpenChecks 0 0 "/mnt-dest-down" "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/container" "dest-down-key" "$UTD_QUBES_TESTVM"
363 | else
364 | postCloseChecks 0 "/mnt-dest-down" "${TEST_STATE["QCRYPT_VM_1"]}" "/tmp/container" "dest-down-key" "$UTD_QUBES_TESTVM"
365 | fi
366 | echo d
367 | assertOutput "$OUT_DEST_DOWN" "$chainDownState" "$chainDownPrefix"
368 |
369 | #device missing chain
370 | echo e
371 | if [ "$chainMissingState" -eq 1 ] ; then
372 | postOpenChecks 1 0 "/mnt-dev-missing" "${TEST_STATE["QCRYPT_VM_2"]}" "/srcmnt/test-folder/container" "dev-missing-key" "${TEST_STATE["QCRYPT_VM_1"]}"
373 | else
374 | local mod=0
375 | [ $chainMissingState -eq 0 ] && mod=2
376 | postCloseChecks $mod "/mnt-dev-missing" "${TEST_STATE["QCRYPT_VM_2"]}" "/srcmnt/test-folder/container" "dev-missing-key" "${TEST_STATE["QCRYPT_VM_1"]}"
377 | fi
378 | echo f
379 | assertOutput "$OUT_DEV_MISSING" "$chainMissingState" "$chainMissingPrefix"
380 | echo g
381 | }
382 |
383 | #testPauseUnpause [vm] [sleep time]
384 | function testPauseUnpause {
385 | local vm="$1"
386 | local time="${2:-10}"
387 |
388 | runSC qvm-pause "$vm"
389 | [ $status -eq 0 ]
390 | sleep $time
391 |
392 | runSC qvm-unpause "$vm"
393 | [ $status -eq 0 ]
394 | sleep 5
395 | }
396 |
397 | @test "start & stop (valid)" {
398 | skipIfQcryptdRunning
399 |
400 | local target="test-start-01"
401 | local targetFolder="$QCRYPTD_CONF_DIR/$target"
402 | OUT_NONEXISTING="$(mktemp)"
403 | OUT_DEST_DOWN="$(mktemp)"
404 | OUT_DEV_MISSING="$(mktemp)"
405 |
406 | prepareQcryptdStartTest "$targetFolder"
407 |
408 | qvm-shutdown --wait "$UTD_QUBES_TESTVM"
409 | runSL b_dom0_isRunning "$UTD_QUBES_TESTVM"
410 | echo "$output"
411 | [ $status -ne 0 ]
412 |
413 | runSL "$QCRYPTD" -v start "$target"
414 | [ $status -eq 0 ]
415 | [ -n "$output" ]
416 | [[ "$output" != *"ERROR"* ]]
417 |
418 | #make sure no chain went up and none is going up in the next few seconds
419 | checkTestChains 0 0 0
420 | sleep 5
421 | checkTestChains 0 0 0
422 |
423 | [ -f "$QCRYPTD_LOG" ]
424 |
425 | #autostart = false should be working
426 | runSL b_dom0_isRunning "$UTD_QUBES_TESTVM"
427 | [ $status -ne 0 ]
428 |
429 | #add missing destination VM should cause the chain to start
430 | #also copy the remaining test container for the nonexisting-dest chain
431 | runSL b_dom0_ensureRunning "$UTD_QUBES_TESTVM"
432 | [ $status -eq 0 ]
433 | [ -z "$output" ]
434 | runSL b_dom0_copy "$(getFixturePath "1layer01/container")" "$UTD_QUBES_TESTVM" "/tmp/" 0
435 | [ $status -eq 0 ]
436 | [ -z "$output" ]
437 | sleep 5
438 | checkTestChains 1 0 1
439 |
440 | #make sure it's not closed again
441 | sleep 30
442 | checkTestChains 1 0 1
443 |
444 | #add the missing loop device should cause the chain to start
445 | runSL b_dom0_createLoopDeviceIfNecessary "${TEST_STATE["QCRYPT_VM_2"]}" "/tmp/loopfile"
446 | [ $status -eq 0 ]
447 | echo "created loop device: ${TEST_STATE["QCRYPT_VM_2"]}:$output"
448 | [[ "$output" == "/dev/loop0" ]]
449 | sleep 7
450 | checkTestChains 1 1 1
451 |
452 | #shutting down the destination VM should cause the chain to get closed
453 | qvm-shutdown --wait "$UTD_QUBES_TESTVM"
454 | runSL b_dom0_isRunning "$UTD_QUBES_TESTVM"
455 | [ $status -ne 0 ]
456 | sleep 5
457 | checkTestChains 2 1 1
458 |
459 | #stopping the service shouldn't affect anything
460 | runSL "$QCRYPTD" stop
461 | [ $status -eq 0 ]
462 | [ -n "$output" ]
463 | [[ "$output" != *"ERROR"* ]]
464 | sleep 2
465 | checkTestChains 2 1 1 1
466 |
467 | #while the service is stopped, re-write its log for later (avoid multiple writes at the same time)
468 | local err="ERROR: The qcrypt chain dest-down is not working anymore and should be closed. However it seems that the $UTD_QUBES_TESTVM VM is still running."
469 | [ -f "$QCRYPTD_LOG" ]
470 | run sed -i "s/$err/${err/closed. However/closed. However}/g" "$QCRYPTD_LOG" #make sure old log entries cannot be found below
471 |
472 | #re-start the service
473 | runSL "$QCRYPTD" -v start "$target"
474 | [ $status -eq 0 ]
475 | [ -n "$output" ]
476 | [[ "$output" != *"ERROR"* ]]
477 | sleep 20
478 | checkTestChains 2 1 1
479 |
480 | #get the missing destination VM chain back up
481 | runSL b_dom0_ensureRunning "$UTD_QUBES_TESTVM"
482 | [ $status -eq 0 ]
483 | [ -z "$output" ]
484 | runSL b_dom0_copy "$(getFixturePath "1layer01/container")" "$UTD_QUBES_TESTVM" "/tmp/" 0
485 | [ $status -eq 0 ]
486 | [ -z "$output" ]
487 | sleep 7
488 | local prefix="starting"$'\n'"started"$'\n'"closing"$'\n'"closed"$'\n'
489 | checkTestChains 1 1 1 0 "$prefix"
490 |
491 | #pausing the destination VM shouldn't be a problem
492 | testPauseUnpause "$UTD_QUBES_TESTVM"
493 | checkTestChains 1 1 1 0 "$prefix"
494 | runSC assertLogHas "$err"
495 | [ $status -ne 0 ]
496 |
497 | #pausing the source or intermediary VMs must generate a big red error
498 | #we have to wait longer than the qrexec timeout for that though (until then the status command will try to obtain a result)
499 | local qtimeout="$(qubes-prefs default_qrexec_timeout)"
500 | testPauseUnpause "${TEST_STATE["QCRYPT_VM_1"]}" $(( $qtimeout +5 ))
501 | assertLogHas "$err"
502 |
503 | #shut down the VM causing issues
504 | qvm-shutdown --wait "$UTD_QUBES_TESTVM"
505 | sleep 5
506 | checkTestChains 2 1 1 0 "$prefix"
507 |
508 | #stop with close all flag
509 | runSL "$QCRYPTD" -c stop
510 | [ $status -eq 0 ]
511 | [ -n "$output" ]
512 | [[ "$output" != *"ERROR"* ]]
513 | sleep 5
514 | checkTestChains 4 2 3 1 "$prefix"
515 |
516 | #cleanup
517 | rm -f "$targetFolder"/*.ini
518 | rm -f "$OUT_NONEXISTING" "$OUT_DEST_DOWN" "$OUT_DEV_MISSING"
519 | }
520 |
521 | #status and restart only use already tested functionality
522 |
523 | @test "cleanup" {
524 | runCleanup
525 | }
526 |
--------------------------------------------------------------------------------
/tests/shellcheck.bats:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bats
2 | #
3 | #+Bats tests to run shellcheck on qcrypt et al.
4 | #+
5 | #+Copyright (C) 2022 David Hobach GPLv3
6 | #+0.7
7 |
8 | load "test_common"
9 |
10 | function setup {
11 | setupBlib
12 | }
13 |
14 | function runShellcheck {
15 | skipIfCommandMissing "shellcheck"
16 |
17 | local file="$1"
18 | runSC shellcheck -s "bash" -S "warning" "$file"
19 | echo "$output"
20 | [ $status -eq 0 ]
21 | [ -z "$output" ]
22 | }
23 |
24 | @test "shellcheck: qcrypt" {
25 | echo "$B_SCRIPT_DIR"
26 | runShellcheck "$QCRYPT"
27 | }
28 |
29 | @test "shellcheck: qcryptd" {
30 | runShellcheck "$QCRYPTD"
31 | }
32 |
--------------------------------------------------------------------------------
/tests/test_common.bash:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #
3 | #+Test code shared across qcrypt & qcryptd tests.
4 | #+
5 | #+Copyright (C) 2019 David Hobach GPLv3
6 | #+0.3
7 |
8 | QCRYPT="$(readlink -f "$BATS_TEST_DIRNAME/../qcrypt")"
9 | QCRYPTD="$(readlink -f "$BATS_TEST_DIRNAME/../qcryptd")"
10 | QCRYPTD_LOG="${QCRYPTD}.log"
11 | QCRYPTD_CONF_DIR="$(readlink -f "$BATS_TEST_DIRNAME/../conf")"
12 |
13 | function setupBlib {
14 | #NOTE: bats forces us to load blib again every time (if we don't do it here, bats will re-source the entire file for every test anyway)
15 | set +e
16 | source blib
17 | source "$B_LIB_DIR/tests/test_common.bash"
18 | set -e
19 | B_TEST_MODE=0
20 | }
21 |
22 | #meant to be run inside setup()
23 | function setupQcryptTesting {
24 | setupBlib
25 | skipIfNotQubesDom0
26 |
27 | [ -z "$UTD_QUBES_TESTVM" ] && skip "Please specify a static disposable test VM as UTD_QUBES_TESTVM in your user data file $USER_DATA_FILE."
28 |
29 | b_import "os/qubes4/dom0"
30 | b_import "keys"
31 |
32 | #re-use the same VMs for all tests, use fresh ones if some test kills them
33 | loadBlibTestState
34 | recreateTestVMsIfNeeded
35 | echo "QCRYPT_VM_1 = ${TEST_STATE["QCRYPT_VM_1"]}"
36 | echo "QCRYPT_VM_2 = ${TEST_STATE["QCRYPT_VM_2"]}"
37 | echo "UTD_QUBES_TESTVM = $UTD_QUBES_TESTVM"
38 |
39 | #set the GUI mode to non-GUI (we don't want popups)
40 | export QCRYPT_UI_MODE="tty"
41 | }
42 |
43 | #recreateTestVMsIfNeeded
44 | function recreateTestVMsIfNeeded {
45 | if [ -z "${TEST_STATE["QCRYPT_VM_1"]}" ] || ! qvm-check --running "${TEST_STATE["QCRYPT_VM_1"]}" &> /dev/null ; then
46 | TEST_STATE["QCRYPT_VM_1"]="$(b_dom0_startDispVM "$UTD_QUBES_DISPVM_TEMPLATE")"
47 | saveBlibTestState
48 | fi
49 | if [ -z "${TEST_STATE["QCRYPT_VM_2"]}" ] || ! qvm-check --running "${TEST_STATE["QCRYPT_VM_2"]}" &> /dev/null ; then
50 | TEST_STATE["QCRYPT_VM_2"]="$(b_dom0_startDispVM "$UTD_QUBES_DISPVM_TEMPLATE")"
51 | saveBlibTestState
52 | fi
53 | return 0
54 | }
55 |
56 | function skipIfNotRealRoot {
57 | [[ "$(whoami)" != "root" ]] && skip "This test must be run as root."
58 | return 0
59 | }
60 |
61 | function skipIfQcryptdRunning {
62 | "$QCRYPTD" status &> /dev/null && skip "qcryptd appears to be running."
63 | return 0
64 | }
65 |
66 | #getFixturePath [fixture name]
67 | function getFixturePath {
68 | local fixture="$1"
69 | [ -n "$fixture" ] && fixture="/$fixture"
70 | echo "$BATS_TEST_DIRNAME/fixtures$fixture"
71 | }
72 |
73 | #getQcryptKeyFolder [vm]
74 | function getQcryptKeyFolder {
75 | local vm="$1"
76 | local user="$(qvm-prefs "$vm" default_user)"
77 | echo "/home/$user/.qcrypt/keys"
78 | }
79 |
80 | #copyFixture [fixture name] [vm]
81 | #Copy the container fixture to the given VM at /tmp/[fixture name].
82 | function copyFixture {
83 | local fixture="$1"
84 | local fixturePath="$(getFixturePath "$fixture")"
85 | local vm="$2"
86 |
87 | local fixtureContainer="$fixturePath/container"
88 | runSL b_dom0_copy "$fixtureContainer" "$vm" "/tmp/$fixture" 0 1
89 | [ $status -eq 0 ]
90 | [ -z "$output" ]
91 | }
92 |
93 | #readOnlyTest [folder]
94 | #This function is meant to be run inside a VM.
95 | function readOnlyTest {
96 | local folder="$1"
97 | local tfile="$folder/FAILURE.txt"
98 | echo "FAILURE" > "$tfile"
99 | [ $? -eq 0 ] && exit 2
100 |
101 | cat "$tfile"
102 | [ $? -eq 0 ] && exit 3
103 |
104 | exit 0
105 | }
106 |
107 | #meant to be run as the last test of a unit
108 | function runCleanup {
109 | qvm-shutdown "$UTD_QUBES_TESTVM" "${TEST_STATE["QCRYPT_VM_1"]}" "${TEST_STATE["QCRYPT_VM_2"]}" || :
110 |
111 | clearBlibTestState
112 |
113 | #test whether qvm-block ls is still working
114 | #cf. https://github.com/QubesOS/qubes-issues/issues/4940
115 | #sometimes it's also the 2 layer close above
116 | runSL qvm-block ls
117 | [ $status -eq 0 ]
118 | [[ "$output" != *"qubesd"* ]]
119 | }
120 |
121 | #assertQcryptStatus [expected status] [mount point] [source vm] [source file] [key id] [destination vm 1] .. [destination vm n]
122 | #[expected status]: exact status match, each error indicates one missing step towards decryption; -1: the status check should produce an error with a non-zero exit code
123 | function assertQcryptStatus {
124 | local expected="$1"
125 | local mp="$2"
126 | local target="${@: -1}"
127 | shift 2
128 |
129 | local statusParams=""
130 | [ -n "$mp" ] && printf -v statusParams -- '--mp %q' "$mp"
131 | runSL "$QCRYPT" status $statusParams -- "$@"
132 | echo "$output"
133 | [ -n "$output" ]
134 | if [ $expected -eq -1 ] ; then
135 | [[ "$output" == *"ERROR"* ]]
136 | [ $status -ne 0 ]
137 | else
138 | [[ "$output" != *"ERROR"* ]]
139 | [[ "$output" == *"$target"* ]]
140 |
141 | echo "expected status: $expected"
142 | echo "actual status: $status"
143 | [ $status -eq $expected ]
144 | fi
145 | }
146 |
147 | #postOpenChecks [read-only flag] [success file flag] [mount point] [source vm] [source file] [key id] [destination vm 1] .. [destination vm n]
148 | #[mount point]: Can be left empty, if the chain is not mounted.
149 | #[success file flag]: Whether a file named SUCCESS.txt with the content 'SUCCESS' exists at [mount point]/SUCCESS.txt. If it doesn't, it will be created as part of this check (unless the mount pint is read-only or it's not mounted).
150 | function postOpenChecks {
151 | local ro="${1:-1}"
152 | local hasSuccFile="${2:-0}"
153 | local mp="$3"
154 | shift 3
155 | local source="$1"
156 | local key="$2"
157 | local target="${@: -1}"
158 |
159 | #check status
160 | #NOTE: we rely on status here, but that's tested inside its dedicated status test
161 | assertQcryptStatus 0 "$mp" "$@"
162 |
163 | #create the success file if needed (write test)
164 | local succFile="$mp/SUCCESS.txt"
165 | local succFileEsc=""
166 | printf -v succFileEsc '%q' "$succFile"
167 | if [ -n "$mp" ] && [ $ro -ne 0 ] && [ $hasSuccFile -ne 0 ] ; then
168 | runSL b_dom0_qvmRun "$target" "echo SUCCESS > $succFileEsc"
169 | [ $status -eq 0 ]
170 | [ -z "$output" ]
171 | hasSuccFile=0
172 | fi
173 |
174 | #check for the success file
175 | if [ -n "$mp" ] && [ $hasSuccFile -eq 0 ] ; then
176 | runSL b_dom0_qvmRun "$target" "cat $succFileEsc"
177 | [ $status -eq 0 ]
178 | [[ "$output" == "SUCCESS" ]]
179 | fi
180 |
181 | #make sure that we cannot write, if r/o
182 | if [ -n "$mp" ] && [ $ro -eq 0 ] ; then
183 | runSL b_dom0_execFuncIn "$target" "" "readOnlyTest" - - "$mp"
184 | [ $status -eq 0 ]
185 | [ -z "$output" ]
186 | fi
187 |
188 | return 0
189 | }
190 |
191 | #postCloseChecks [expected status mod] [mount point] [source vm] [source file] [key id] [destination vm 1] .. [destination vm n]
192 | #[expected status mod]: Integer modifier for the expected status (default: 0). Only required under very special circumstances (e.g. source file missing).
193 | function postCloseChecks {
194 | local mod="${1:-0}"
195 | local mp="$2"
196 | local sourceVM="$3"
197 | shift 2
198 |
199 | local numDst=$(( $# -3 ))
200 | #expected status: device not attached & not decrypted for each VM (VMs running & keys available though), no loop device in source VM anymore
201 | local eStatus=$(( $numDst * 2 + 1 ))
202 | [ -n "$mp" ] && ((++eStatus)) #NOTE: for simplicity, --mp "" and no mount point are not distinguished here
203 |
204 | #add run status info
205 | declare -a vms=("$sourceVM" "${@:4}")
206 | local vm=
207 | for vm in "${vms[@]}" ; do
208 | #not running: VM down & missing key/source file --> +2
209 | qvm-check --running "$vm" &> /dev/null || eStatus=$(( $eStatus +2))
210 | done
211 |
212 | #add modifier
213 | eStatus=$(( $eStatus + $mod ))
214 |
215 | assertQcryptStatus "$eStatus" "$mp" "$@"
216 | }
217 |
218 | #assertLogHas [list]
219 | #Check whether the log has any of the provided strings.
220 | #[list]: Newline-separated list of strings to check for.
221 | function assertLogHas {
222 | local str="$1"
223 | grep -Fq "$str" "$QCRYPTD_LOG"
224 | }
225 |
--------------------------------------------------------------------------------