├── .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 | --------------------------------------------------------------------------------