From 05dab994196b38dca39fa08081db7c22b35f1a18 Mon Sep 17 00:00:00 2001 From: Ville Takio Date: Mon, 14 Sep 2026 20:07:56 +0300 Subject: [PATCH] luci-app-veracrypt: add optional LuCI UI Web UI for the console veracrypt package (packages feed): https://github.com/openwrt/packages/pull/30508 Not bundled into veracrypt; apk add luci-app-veracrypt depends on +veracrypt. Calls veracrypt --text only. The current password is passed on stdin (--stdin), never --password. New password, hidden volume password and token PIN are fed on stdin, not via --new-password, --protection-password or --token-pin. CLI reference: https://www.veracrypt.fr/en/Command%20Line%20Usage.html Signed-off-by: Ville Takio --- applications/luci-app-veracrypt/LICENSE | 353 +++ applications/luci-app-veracrypt/Makefile | 20 + applications/luci-app-veracrypt/README.md | 13 + .../luci-static/resources/view/veracrypt.js | 2047 +++++++++++++++++ .../root/etc/config/veracrypt | 10 + .../root/usr/libexec/rpcd/luci.veracrypt | 1730 ++++++++++++++ .../share/luci/menu.d/luci-app-veracrypt.json | 13 + .../share/rpcd/acl.d/luci-app-veracrypt.json | 20 + 8 files changed, 4206 insertions(+) create mode 100644 applications/luci-app-veracrypt/LICENSE create mode 100644 applications/luci-app-veracrypt/Makefile create mode 100644 applications/luci-app-veracrypt/README.md create mode 100644 applications/luci-app-veracrypt/htdocs/luci-static/resources/view/veracrypt.js create mode 100644 applications/luci-app-veracrypt/root/etc/config/veracrypt create mode 100755 applications/luci-app-veracrypt/root/usr/libexec/rpcd/luci.veracrypt create mode 100644 applications/luci-app-veracrypt/root/usr/share/luci/menu.d/luci-app-veracrypt.json create mode 100644 applications/luci-app-veracrypt/root/usr/share/rpcd/acl.d/luci-app-veracrypt.json diff --git a/applications/luci-app-veracrypt/LICENSE b/applications/luci-app-veracrypt/LICENSE new file mode 100644 index 000000000000..c28e7a83d906 --- /dev/null +++ b/applications/luci-app-veracrypt/LICENSE @@ -0,0 +1,353 @@ +luci-app-veracrypt +Copyright (C) 2026 Ville Takio + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; version 2 of the License only. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, see . + + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, see . + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Moe Ghoul, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/applications/luci-app-veracrypt/Makefile b/applications/luci-app-veracrypt/Makefile new file mode 100644 index 000000000000..1a3efba8734e --- /dev/null +++ b/applications/luci-app-veracrypt/Makefile @@ -0,0 +1,20 @@ +include $(TOPDIR)/rules.mk + +LUCI_TITLE:=LuCI support for VeraCrypt +LUCI_DESCRIPTION:=Optional web UI for console VeraCrypt. Uses veracrypt --text only; no VeraCrypt GUI toolkit. +# CLI package: https://github.com/openwrt/packages/pull/30508 +LUCI_DEPENDS:=+luci-base +veracrypt +LUCI_PKGARCH:=all +LUCI_URL:=https://www.veracrypt.fr/ + +PKG_LICENSE:=GPL-2.0-only +PKG_LICENSE_FILES:=LICENSE +PKG_MAINTAINER:=Ville Takio + +define Package/luci-app-veracrypt/conffiles +/etc/config/veracrypt +endef + +include ../../luci.mk + +# call BuildPackage - OpenWrt buildroot signature diff --git a/applications/luci-app-veracrypt/README.md b/applications/luci-app-veracrypt/README.md new file mode 100644 index 000000000000..f1ee7fb4d695 --- /dev/null +++ b/applications/luci-app-veracrypt/README.md @@ -0,0 +1,13 @@ +# luci-app-veracrypt + +Optional LuCI UI for console VeraCrypt. It is not part of the `veracrypt` +package. CLI package: https://github.com/openwrt/packages/pull/30508 + +`DEPENDS` includes `+veracrypt`, so `apk add luci-app-veracrypt` pulls the +CLI. The pages call `veracrypt --text` only (no VeraCrypt GUI toolkit). + +The current password is passed with `veracrypt --stdin`, never `--password`. +A new password, hidden-volume password or token PIN is also fed on stdin, +not `--new-password` / `--protection-password` / `--token-pin` on argv. + +CLI reference: https://www.veracrypt.fr/en/Command%20Line%20Usage.html diff --git a/applications/luci-app-veracrypt/htdocs/luci-static/resources/view/veracrypt.js b/applications/luci-app-veracrypt/htdocs/luci-static/resources/view/veracrypt.js new file mode 100644 index 000000000000..f421b48972ba --- /dev/null +++ b/applications/luci-app-veracrypt/htdocs/luci-static/resources/view/veracrypt.js @@ -0,0 +1,2047 @@ +'use strict'; +/* SPDX-License-Identifier: GPL-2.0-only */ +'require view'; +'require form'; +'require uci'; +'require rpc'; +'require ui'; +'require poll'; + +var callStatus = rpc.declare({ + object: 'luci.veracrypt', + method: 'status' +}); + +var callListDev = rpc.declare({ + object: 'luci.veracrypt', + method: 'listdev' +}); + +var callListDir = rpc.declare({ + object: 'luci.veracrypt', + method: 'listdir', + params: [ 'path' ] +}); + +var callMkdir = rpc.declare({ + object: 'luci.veracrypt', + method: 'mkdir', + params: [ 'path' ] +}); + +var callRm = rpc.declare({ + object: 'luci.veracrypt', + method: 'rm', + params: [ 'path', 'recursive' ] +}); + +var RUN_PARAMS = [ + 'action', 'name', 'volume', 'mountpoint', 'password', 'new_password', + 'pim', 'new_pim', 'hash', 'new_hash', 'encryption', 'filesystem', + 'fs_options', 'keyfiles', 'new_keyfiles', 'protect_hidden', + 'protection_password', 'protection_pim', 'protection_hash', + 'protection_keyfiles', 'slot', 'size', 'volume_type', 'random_source', + 'token_lib', 'token_pin', 'mount_options', 'auto_mount', 'force', + 'quick', 'no_size_check', 'legacy_password_maxlength', + 'allow_insecure_mount', 'fsck_auto', 'backup_file' +]; + +function timeoutSec() { + var t = parseInt(uci.get('veracrypt', 'main', 'timeout'), 10); + if (isNaN(t) || t < 300) + t = 300; + return t; +} + +function fmtClock(sec) { + if (sec < 0) + sec = 0; + var m = Math.floor(sec / 60); + var s = sec % 60; + return '%d:%02d'.format(m, s); +} + +function callRunWithTimeout() { + return rpc.declare({ + object: 'luci.veracrypt', + method: 'run', + timeout: timeoutSec() * 1000, + params: RUN_PARAMS + }); +} + +function invokeRun(opts) { + opts = opts || {}; + return callRunWithTimeout()( + opts.action || '', opts.name || '', opts.volume || '', opts.mountpoint || '', + opts.password || '', opts.new_password || '', opts.pim || '', opts.new_pim || '', + opts.hash || '', opts.new_hash || '', opts.encryption || '', opts.filesystem || '', + opts.fs_options || '', opts.keyfiles || '', opts.new_keyfiles || '', + opts.protect_hidden || '', opts.protection_password || '', opts.protection_pim || '', + opts.protection_hash || '', opts.protection_keyfiles || '', opts.slot || '', + opts.size || '', opts.volume_type || '', opts.random_source || '', + opts.token_lib || '', opts.token_pin || '', opts.mount_options || '', + opts.auto_mount || '', opts.force || '', opts.quick || '', + opts.no_size_check || '', opts.legacy_password_maxlength || '', + opts.allow_insecure_mount || '', opts.fsck_auto || '', opts.backup_file || '' + ); +} + +function callJobWithTimeout() { + return rpc.declare({ + object: 'luci.veracrypt', + method: 'job', + timeout: Math.max(20000, Math.min(60000, timeoutSec() * 1000)) + }); +} + +var callTools = rpc.declare({ + object: 'luci.veracrypt', + method: 'tools' +}); + +var callJobLog = rpc.declare({ + object: 'luci.veracrypt', + method: 'job_log' +}); + +var callJobAbort = rpc.declare({ + object: 'luci.veracrypt', + method: 'job_abort' +}); + +var callJobDismount = rpc.declare({ + object: 'luci.veracrypt', + method: 'job_dismount' +}); + +var callJobAnswer = rpc.declare({ + object: 'luci.veracrypt', + method: 'job_answer', + params: [ 'answer' ] +}); + +function missingPackages(t, fs) { + if (!fs || fs === 'none') + return []; + var missing = []; + if (fs === 'ext4' || fs === 'ext3' || fs === 'ext2') { + if (!t || !t.has_mkfs_ext4) + missing.push('e2fsprogs'); + if (!t || !t.has_kmod_ext4) + missing.push('kmod-fs-ext4'); + } + else if (fs === 'vfat') { + if (!t || !t.has_mkfs_vfat) + missing.push('dosfstools'); + if (!t || !t.has_kmod_vfat) + missing.push('kmod-fs-vfat'); + } + else if (fs === 'ntfs') { + if (!t || !t.has_mkfs_ntfs) + missing.push('ntfs-3g'); + if (!t || !t.has_kmod_ntfs) + missing.push('kmod-fs-ntfs3'); + } + else if (fs === 'exfat') { + if (!t || !t.has_mkfs_exfat) + missing.push('exfatprogs'); + if (!t || !t.has_kmod_exfat) + missing.push('kmod-fs-exfat'); + } + return missing; +} + +function packagesForFsck(fs) { + switch (fs) { + case 'ext4': + case 'ext3': + case 'ext2': + return [ 'e2fsprogs' ]; + case 'vfat': + return [ 'dosfstools' ]; + case 'ntfs': + return [ 'ntfs-3g' ]; + case 'exfat': + return [ 'exfatprogs' ]; + default: + return [ 'e2fsprogs' ]; + } +} + +function fsckToolsReady(t, fs) { + if (!t) + return false; + if (!fs || fs === 'none') + return !!(t.has_e2fsck || t.has_fsck_ext4 || t.has_fsck_fat || t.has_fsck_exfat || t.has_ntfsfix || t.has_fsck); + if (fs === 'ext4' || fs === 'ext3' || fs === 'ext2') + return !!(t.has_e2fsck || t.has_fsck_ext4); + if (fs === 'vfat') + return !!t.has_fsck_fat; + if (fs === 'ntfs') + return !!t.has_ntfsfix; + if (fs === 'exfat') + return !!t.has_fsck_exfat; + return !!t.has_fsck; +} + +function setLogText(logEl, text) { + if (!logEl) + return; + var nearBottom = (logEl.scrollHeight - logEl.scrollTop - logEl.clientHeight) < 48; + logEl.textContent = text || ''; + if (nearBottom) + logEl.scrollTop = logEl.scrollHeight; +} + +function copyText(text) { + text = text || ''; + if (navigator.clipboard && navigator.clipboard.writeText) + return navigator.clipboard.writeText(text); + var ta = document.createElement('textarea'); + ta.value = text; + ta.style.position = 'fixed'; + ta.style.left = '-9999px'; + document.body.appendChild(ta); + ta.select(); + try { document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + return Promise.resolve(); +} + +function saveTextFile(name, text) { + var blob = new Blob([ text || '' ], { type: 'text/plain;charset=utf-8' }); + var url = URL.createObjectURL(blob); + var a = document.createElement('a'); + a.href = url; + a.download = name; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + window.setTimeout(function() { URL.revokeObjectURL(url); }, 1500); +} + +function logStamp(action) { + var d = new Date(); + function z(n) { return (n < 10 ? '0' : '') + n; } + return 'veracrypt-' + (action || 'job') + '-' + + d.getFullYear() + z(d.getMonth() + 1) + z(d.getDate()) + '-' + + z(d.getHours()) + z(d.getMinutes()) + z(d.getSeconds()) + '.log'; +} + +function abortButton(ctl) { + return E('button', { + 'type': 'button', + 'class': 'btn cbi-button-remove', + 'title': _('Abort / cancel. Stop the job on the router (veracrypt, fsck, or apk) and close this dialog.'), + 'click': function() { + ctl.stopped = true; + callJobAbort().then(function(res) { + ui.hideModal(); + showResult({ ok: false, error: (res && res.output) || _('Aborted.') }); + }).catch(function(err) { + ui.hideModal(); + ui.addNotification(null, E('p', err.message || _('Aborted.')), 'warning'); + }); + } + }, _('Abort')); +} + +function installPackages(list) { + var limit = timeoutSec(); + var statusEl = E('p'); + var elapsed = 0; + var left = limit; + var ctl = { stopped: false }; + function paint() { + statusEl.textContent = _('apk add %s — elapsed %s, timeout in %s').format(list.join(' '), fmtClock(elapsed), fmtClock(left)); + } + ui.showModal(_('Install packages'), [ + statusEl, + E('div', { 'class': 'right' }, [ abortButton(ctl) ]) + ]); + paint(); + var iv = window.setInterval(function() { + elapsed++; + left--; + paint(); + }, 1000); + var inst = rpc.declare({ + object: 'luci.veracrypt', + method: 'pkg_install', + timeout: limit * 1000, + params: [ 'packages' ] + }); + return inst(list.join(' ')).then(function(res) { + if (ctl.stopped) + return { ok: false, aborted: true }; + if (res && res.pending) + return waitJob(left, statusEl, null, ctl); + return res; + }).then(function(res) { + window.clearInterval(iv); + if (ctl.stopped) + return false; + ui.hideModal(); + showResult(res); + return res && res.ok !== false; + }).catch(function(err) { + window.clearInterval(iv); + if (ctl.stopped) + return false; + ui.hideModal(); + ui.addNotification(null, E('p', err.message || String(err)), 'error'); + return false; + }); +} + +function ensureFsPackages(o) { + if (o.action !== 'create') + return Promise.resolve(true); + var fs = o.filesystem || 'none'; + if (!fs || fs === 'none') + return Promise.resolve(true); + return callTools().then(function(t) { + var pkgs = missingPackages(t, fs); + if (!pkgs.length) + return true; + return new Promise(function(resolve) { + ui.showModal(_('Missing tools for %s').format(fs), [ + E('p', _('Creating a volume with an inner %s filesystem needs the packages that are not installed: %s. Already-present mkfs tools and kmods are not listed. Install with apk add, or create with filesystem=none and format after mapping.').format(fs, pkgs.join(' '))), + E('div', { 'class': 'right' }, [ + E('button', { + 'type': 'button', + 'class': 'btn', + 'title': _('Close this dialog. The volume is not created.'), + 'click': function() { ui.hideModal(); resolve(false); } + }, _('Cancel')), + ' ', + E('button', { + 'type': 'button', + 'class': 'btn', + 'title': _('Create the container with --filesystem=none. Format the inner filesystem later after mount.'), + 'click': function() { + ui.hideModal(); + o.filesystem = 'none'; + resolve(true); + } + }, _('Create with filesystem=none')), + ' ', + E('button', { + 'type': 'button', + 'class': 'btn cbi-button-apply', + 'title': _('apk add %s, then create the volume with the chosen inner filesystem.').format(pkgs.join(' ')), + 'click': function() { + ui.hideModal(); + installPackages(pkgs).then(function(ok) { resolve(ok); }); + } + }, _('apk add and continue')) + ]) + ]); + }); + }); +} + +function ensureFsckPackages(o) { + if (o.action !== 'fsck') + return Promise.resolve(true); + var fs = o.filesystem || ''; + var pkgs = packagesForFsck(fs); + return callTools().then(function(t) { + if (fsckToolsReady(t, fs)) + return true; + return new Promise(function(resolve) { + ui.showModal(_('Missing fsck tools'), [ + E('p', _('Checking a volume cannot be done without the matching fsck tool. The app decrypts with --filesystem=none, runs fsck on the mapper or loop device, then dismounts. Install: %s (e2fsprogs for ext*, dosfstools for FAT, exfatprogs for exFAT, ntfs-3g for NTFS).').format(pkgs.join(' '))), + E('div', { 'class': 'right' }, [ + E('button', { + 'type': 'button', + 'class': 'btn', + 'title': _('Close this dialog. fsck is not run.'), + 'click': function() { ui.hideModal(); resolve(false); } + }, _('Cancel')), + ' ', + E('button', { + 'type': 'button', + 'class': 'btn cbi-button-apply', + 'title': _('Install %s with apk add. fsck cannot run without these tools.').format(pkgs.join(' ')), + 'click': function() { + ui.hideModal(); + installPackages(pkgs).then(function(ok) { resolve(ok); }); + } + }, _('apk add')) + ]) + ]); + }); + }); +} + +function showResult(res) { + var err = res && res.error ? String(res.error) : ''; + if (err.indexOf('PKCS') !== -1 || err.indexOf('Security Tokens') !== -1) + err = _('No PKCS #11 library loaded. Set the library path under Timeouts → Security token library (for example /usr/lib/libykcs11.so). This app has no Settings > Security Tokens.'); + if (res && res.need_packages) { + var pkgs = String(res.need_packages).split(/[\s,]+/).filter(Boolean); + ui.showModal(_('Missing fsck tools'), [ + E('pre', err || _('Checking a volume cannot be done without the matching fsck tool.')), + E('p', _('apk add %s').format(pkgs.join(' '))), + E('div', { 'class': 'right' }, [ + E('button', { + 'type': 'button', + 'class': 'btn', + 'title': _('Close this dialog. fsck is not run.'), + 'click': ui.hideModal + }, _('Cancel')), + ' ', + E('button', { + 'type': 'button', + 'class': 'btn cbi-button-apply', + 'title': _('Install %s with apk add. fsck cannot run without these tools.').format(pkgs.join(' ')), + 'click': function() { + ui.hideModal(); + installPackages(pkgs); + } + }, _('apk add')) + ]) + ]); + return; + } + if (!res || res.ok === false) + ui.addNotification(null, E('pre', err || _('Command failed')), 'error'); + else if (res.output) + ui.addNotification(null, E('pre', res.output), 'info'); + else + ui.addNotification(null, E('p', _('OK')), 'info'); +} + +function field(type, attrs) { + attrs = attrs || {}; + attrs.type = type || 'text'; + attrs.style = (attrs.style || '') + ';width:100%'; + return E('input', attrs); +} + +function select(values, cur) { + var s = E('select', { 'style': 'width:100%' }); + values.forEach(function(v) { + var val = Array.isArray(v) ? v[0] : v; + var lab = Array.isArray(v) ? v[1] : v; + var opt = E('option', { 'value': val }, lab); + if (String(cur) === String(val)) + opt.selected = true; + s.appendChild(opt); + }); + return s; +} + +function pathRow(label, value, dirsOnly, opts) { + opts = opts || {}; + var allowDirValue = !!opts.allowDirValue || !!dirsOnly; + var inp = field('text', { + 'value': value || '', + 'placeholder': dirsOnly ? '/mnt/Buffalo' : '/mnt/sda2/media.tc' + }); + var listing = E('div', { 'style': 'max-height:180px;overflow:auto;margin-top:6px' }); + var status = E('p', { 'class': 'cbi-map-descr' }); + var newName = E('input', { + 'type': 'text', + 'placeholder': dirsOnly ? 'Buffalo' : 'newdir', + 'style': 'width:60%' + }); + var browse = value || '/mnt'; + if (!dirsOnly && browse.lastIndexOf('/') > 0) + browse = browse.replace(/\/[^/]+$/, '') || '/mnt'; + + function setStatus(t) { + status.textContent = t || ''; + } + + function load(path) { + if (!path) + path = '/mnt'; + setStatus(_('Listing %s …').format(path)); + return callListDir(path).then(function(res) { + while (listing.firstChild) + listing.removeChild(listing.firstChild); + if (!res || res.ok === false) { + setStatus(res && res.error ? res.error : _('Cannot list directory')); + var parent = String(path || '').replace(/\/+$/, '').replace(/\/[^/]+$/, '') || '/mnt'; + if (parent !== path) + return load(parent); + return; + } + browse = res.path || path; + setStatus(_('Browsing %s. Create or delete here without leaving this dialog.').format(browse)); + (res.entries || []).forEach(function(ent) { + if (!ent || !ent.name) + return; + var isDir = ent.type === 'dir'; + var isDot = ent.name === '..'; + var row = E('div', { 'style': 'white-space:nowrap;margin:1px 0' }); + if (!dirsOnly || isDir) { + row.appendChild(E('button', { + 'type': 'button', + 'class': 'btn', + 'style': 'margin:1px', + 'title': isDir + ? _('Open directory %s').format(ent.path) + : _('Select file %s').format(ent.path), + 'click': function(ev) { + if (ev) + ev.preventDefault(); + if (isDir) { + if (allowDirValue && !isDot) + inp.value = ent.path; + load(ent.path); + } + else { + inp.value = ent.path; + } + } + }, isDir ? ent.name + '/' : ent.name)); + } + else { + row.appendChild(E('span', { 'style': 'margin:1px' }, ent.name)); + } + if (!isDot) { + row.appendChild(E('button', { + 'type': 'button', + 'class': 'btn cbi-button-remove', + 'style': 'margin:1px', + 'title': _('Delete %s after confirmation. The dialog stays open.').format(ent.path), + 'click': function(ev) { + if (ev) { + ev.preventDefault(); + ev.stopPropagation(); + } + removePath(ent.path, isDir); + } + }, _('Delete'))); + } + listing.appendChild(row); + }); + }).catch(function(err) { + setStatus(err.message || String(err)); + }); + } + + function makeDir(ev) { + if (ev) { + ev.preventDefault(); + ev.stopPropagation(); + } + var n = (newName.value || '').trim().replace(/\/+$/, ''); + if (!n) { + setStatus(_('Type a directory name, then Create directory.')); + return; + } + var p = n.charAt(0) === '/' ? n : String(browse || '/mnt').replace(/\/+$/, '') + '/' + n.replace(/^\/+/, ''); + setStatus(_('Creating %s …').format(p)); + return callMkdir(p).then(function(res) { + if (!res || res.ok === false) { + setStatus(res && res.error ? res.error : _('mkdir failed')); + return; + } + inp.value = p; + newName.value = ''; + return load(p); + }).catch(function(err) { + setStatus(err.message || String(err)); + }); + } + + function removePath(p, isDir) { + p = String(p || '').trim(); + if (!p) { + setStatus(_('Nothing to delete.')); + return; + } + var msg = isDir + ? _('Delete directory %s? This cannot be undone.').format(p) + : _('Delete file %s? This cannot be undone.').format(p); + if (!window.confirm(msg)) + return; + setStatus(_('Deleting %s …').format(p)); + return callRm(p, '').then(function(res) { + if (res && res.need_recursive) { + if (!window.confirm(_('Directory %s is not empty. Delete it and all contents? This cannot be undone.').format(p))) { + setStatus(_('Delete cancelled.')); + return; + } + return callRm(p, '1'); + } + return res; + }).then(function(res) { + if (!res) + return; + if (!res.ok && res.ok !== 1) { + setStatus(res.error || _('delete failed')); + return; + } + if (inp.value === p) + inp.value = browse || ''; + setStatus(_('Deleted %s.').format(p)); + return load(browse); + }).catch(function(err) { + setStatus(err.message || String(err)); + }); + } + + newName.addEventListener('keydown', function(ev) { + if (ev.key === 'Enter' || ev.keyCode === 13) { + ev.preventDefault(); + ev.stopPropagation(); + makeDir(ev); + } + }); + + load(browse); + + return { + node: E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, label), + E('div', { 'class': 'cbi-value-field' }, [ + E('p', { 'class': 'cbi-map-descr' }, + dirsOnly + ? _('Type the directory, browse below, or create a directory. Create and per-row Delete stay in this dialog.') + : (allowDirValue + ? _('Type a path, or browse and click a file or directory. A directory is a valid keyfile: all non-hidden files in it are used.') + : _('Type the container path, or browse and click the file. You can create directories here.')) + ), + inp, + status, + listing, + E('div', {}, [ + newName, + ' ', + E('button', { + 'type': 'button', + 'class': 'btn', + 'title': _('Create the named directory under the current folder. The dialog stays open.'), + 'click': makeDir + }, _('Create directory')) + ]) + ]) + ]), + getValue: function() { + return (inp.value || '').trim() || value || ''; + }, + getDir: function() { + return browse || '/mnt'; + } + }; +} + +function parseTokenKeyfiles(text) { + var out = []; + String(text || '').split(/\r?\n/).forEach(function(line) { + line = line.trim(); + var m = line.match(/(token:\/\/slot\/[0-9]+\/file\/\S+|emv:\/\/slot\/[0-9]+)/); + if (m) + out.push(m[1]); + }); + return out; +} + +function keyfilesRow(label) { + var items = []; + var listEl = E('div'); + var tokenBox = E('div'); + var picker = pathRow(_('Browse'), '', false, { allowDirValue: true }); + + function renderList() { + while (listEl.firstChild) + listEl.removeChild(listEl.firstChild); + if (!items.length) { + listEl.appendChild(E('p', { 'class': 'cbi-map-descr' }, _('No keyfiles selected.'))); + return; + } + items.forEach(function(p, idx) { + listEl.appendChild(E('div', { 'style': 'white-space:nowrap;margin:2px 0' }, [ + E('span', {}, p), + ' ', + E('button', { + 'type': 'button', + 'class': 'btn cbi-button-remove', + 'title': _('Remove %s from the keyfile list. The file is not deleted.').format(p), + 'click': function() { + items.splice(idx, 1); + renderList(); + } + }, _('Remove')) + ])); + }); + } + + function addPath(p) { + p = String(p || '').trim(); + if (!p) + return; + if (items.indexOf(p) === -1) + items.push(p); + renderList(); + } + + function addTokenFiles() { + var lib = uci.get('veracrypt', 'main', 'token_lib') || ''; + if (!lib) { + ui.addNotification(null, E('p', + _('No PKCS #11 library path is set. Use Settings → Security token library (example: /usr/lib/libykcs11.so).') + ), 'warning'); + return; + } + while (tokenBox.firstChild) + tokenBox.removeChild(tokenBox.firstChild); + tokenBox.appendChild(E('p', { 'class': 'cbi-map-descr' }, _('Listing token keyfiles…'))); + return invokeRun({ action: 'list-token-keyfiles', token_lib: lib }).then(function(res) { + while (tokenBox.firstChild) + tokenBox.removeChild(tokenBox.firstChild); + var paths = parseTokenKeyfiles((res && (res.output || res.error)) || ''); + if (!paths.length) { + tokenBox.appendChild(E('p', { 'class': 'cbi-map-descr' }, + (res && res.error) || _('No token keyfiles listed. Set Settings → Security token library, or import a keyfile onto the token.'))); + return; + } + tokenBox.appendChild(E('p', { 'class': 'cbi-map-descr' }, + _('Token keyfiles. Click to add. VeraCrypt never modifies keyfile contents.'))); + paths.forEach(function(p) { + tokenBox.appendChild(E('button', { + 'type': 'button', + 'class': 'btn', + 'style': 'margin:2px', + 'title': _('Add %s as a keyfile.').format(p), + 'click': function() { addPath(p); } + }, p)); + }); + }).catch(function(err) { + while (tokenBox.firstChild) + tokenBox.removeChild(tokenBox.firstChild); + tokenBox.appendChild(E('p', { 'class': 'cbi-map-descr' }, err.message || String(err))); + }); + } + + renderList(); + return { + node: E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, label || _('Keyfiles')), + E('div', { 'class': 'cbi-value-field' }, [ + E('p', { 'class': 'cbi-map-descr' }, + _('VeraCrypt never modifies keyfile contents. You can select more than one keyfile (the order does not matter). If you add a folder, all non-hidden files found in it will be used as keyfiles.')), + listEl, + tokenBox, + picker.node, + E('div', {}, [ + E('button', { + 'type': 'button', + 'class': 'btn', + 'title': _('Add the selected file to the keyfile list.'), + 'click': function() { addPath(picker.getValue()); } + }, _('Add file')), + ' ', + E('button', { + 'type': 'button', + 'class': 'btn', + 'title': _('Add the current directory as a keyfile. All non-hidden files in it will be used.'), + 'click': function() { addPath(picker.getValue() || picker.getDir()); } + }, _('Add directory')), + ' ', + E('button', { + 'type': 'button', + 'class': 'btn', + 'title': _('Select keyfiles stored on a security token or smart card (token://).'), + 'click': addTokenFiles + }, _('Add token files')) + ]) + ]) + ]), + getValue: function() { + return items.join(','); + } + }; +} + +function val(el) { + return el && el.value != null ? String(el.value) : ''; +} + +function flag(el) { + return el && el.checked ? '1' : ''; +} + +function actionHint(action) { + switch (action) { + case 'mount': + return _('This operation can take several minutes on a slow CPU with little RAM.'); + case 'create': + return _('Creating a volume, especially without quick format, can take several minutes on a slow CPU with little RAM.'); + case 'fsck': + return _('Decrypting the volume and running fsck can take several minutes on a slow CPU with little RAM.'); + case 'test': + return _('Algorithm self-tests can take several minutes on a slow CPU with little RAM.'); + case 'change': + return _('Changing the password re-derives the header and can take several minutes on a slow CPU with little RAM.'); + case 'backup-headers': + case 'restore-headers': + return _('Header backup or restore can take several minutes on a slow CPU with little RAM.'); + case 'create-keyfile': + return _('Writing a random keyfile is usually quick.'); + case 'unmount': + return ''; + default: + return ''; + } +} + +function workingLine(action, elapsed, left) { + if (action === 'unmount') + return _('Unmounting… elapsed %s. Timeout in %s.').format(fmtClock(elapsed), fmtClock(left)); + return _('Working… elapsed %s. Timeout in %s.').format(fmtClock(elapsed), fmtClock(left)); +} + +function waitJob(limit, statusEl, logEl, ctl, action) { + var left = limit; + var elapsed = 0; + var job = callJobWithTimeout(); + ctl = ctl || {}; + action = action || ''; + var hint = actionHint(action); + + function paint() { + var line = workingLine(action, elapsed, left); + if (hint) + line += ' ' + hint; + statusEl.textContent = line; + } + paint(); + + var iv = window.setInterval(function() { + elapsed++; + left--; + paint(); + }, 1000); + + function poll() { + if (ctl.stopped) { + window.clearInterval(iv); + return { ok: false, aborted: true, error: _('Aborted.') }; + } + if (left <= 0) { + window.clearInterval(iv); + return { + ok: false, + error: _('Timed out after %s. veracrypt may still be running on the router. Use Abort next time, or raise Timeouts.') + .format(fmtClock(limit)) + }; + } + return job().then(function(res) { + if (ctl.stopped) { + window.clearInterval(iv); + return { ok: false, aborted: true, error: _('Aborted.') }; + } + if (logEl && res && res.output) + setLogText(logEl, res.output); + if (res && res.pending) + return new Promise(function(resolve) { + window.setTimeout(function() { resolve(poll()); }, 1000); + }); + window.clearInterval(iv); + return res; + }).catch(function(err) { + window.clearInterval(iv); + throw err; + }); + } + return poll(); +} + +function showCloseDialog(title, text) { + ui.showModal(title || _('VeraCrypt'), [ + E('pre', { + 'style': 'max-height:360px;overflow:auto;white-space:pre-wrap;user-select:text;background:var(--background-color-high, #111);padding:8px' + }, text || ''), + E('div', { 'class': 'right' }, [ + E('button', { + 'type': 'button', + 'class': 'btn', + 'title': _('Close this dialog.'), + 'click': ui.hideModal + }, _('Close')) + ]) + ]); +} + +function runAction(opts) { + var limit = timeoutSec(); + var action = opts.action || ''; + if (action === 'help' || action === 'version') { + return invokeRun(opts).then(function(res) { + showCloseDialog(action === 'help' ? _('Help') : _('Version'), + (res && (res.output || res.error)) || (res && res.ok !== false ? _('OK') : _('Command failed'))); + return res; + }).catch(function(err) { + showCloseDialog(_('VeraCrypt'), err.message || String(err)); + }); + } + var statusEl = E('p'); + var hintEl = E('p', { 'class': 'cbi-map-descr' }, actionHint(action)); + var elapsed = 0; + var left = limit; + var ctl = { stopped: false }; + var showLog = (action === 'fsck' || action === 'create' || action === 'change' || + action === 'backup-headers' || action === 'restore-headers' || action === 'test' || + action === 'create-keyfile' || action === 'list-token-keyfiles'); + var showCopySave = (action === 'fsck' || action === 'create' || action === 'change' || + action === 'backup-headers' || action === 'restore-headers'); + var autoClose = (action === 'unmount' || action === 'mount'); + function paint() { + statusEl.textContent = workingLine(action, elapsed, left); + } + var logEl = E('pre', { + 'style': 'max-height:280px;overflow:auto;white-space:pre-wrap;user-select:text;background:var(--background-color-high, #111);padding:8px' + + (showLog ? '' : ';display:none') + }); + var ynBox = E('p'); + if (action === 'fsck' && opts.fsck_auto !== '1') { + ynBox.appendChild(E('p', { 'class': 'cbi-map-descr' }, + _('fsck is interactive. Press y or n for each prompt.'))); + ynBox.appendChild(E('button', { + 'type': 'button', + 'class': 'btn cbi-button-apply', + 'title': _('Answer yes to the current fsck prompt.'), + 'click': function() { callJobAnswer('y'); } + }, _('y'))); + ynBox.appendChild(E('span', {}, ' ')); + ynBox.appendChild(E('button', { + 'type': 'button', + 'class': 'btn', + 'title': _('Answer no to the current fsck prompt.'), + 'click': function() { callJobAnswer('n'); } + }, _('n'))); + } + var closeBtn = E('button', { + 'type': 'button', + 'class': 'btn', + 'style': 'display:none', + 'title': action === 'fsck' + ? _('Unmount the decrypted volume and close this dialog.') + : _('Close this dialog.'), + 'click': function() { + function done() { + ui.hideModal(); + if (action === 'fsck' || action === 'mount' || action === 'create' || action === 'unmount' || action === 'test') + window.location.reload(); + } + if (action === 'fsck') { + statusEl.textContent = _('Unmounting…'); + callJobDismount().then(done).catch(done); + return; + } + done(); + } + }, _('Close')); + var copyBtn = E('button', { + 'type': 'button', + 'class': 'btn', + 'style': showCopySave ? '' : 'display:none', + 'title': _('Copy the log to the clipboard.'), + 'click': function() { + var t = logEl.textContent || ''; + copyText(t).then(function() { + statusEl.textContent = _('Log copied to clipboard.'); + }).catch(function() { + statusEl.textContent = _('Copy failed. Select the log and copy it yourself.'); + }); + } + }, _('Copy log')); + var abortEl = abortButton(ctl); + var saveBtn = E('button', { + 'type': 'button', + 'class': 'btn', + 'style': showCopySave ? '' : 'display:none', + 'title': _('Save the log as a text file on this computer.'), + 'click': function() { + callJobLog().then(function(res) { + var t = (res && res.output) || logEl.textContent || ''; + saveTextFile(logStamp(action), t); + }).catch(function() { + saveTextFile(logStamp(action), logEl.textContent || ''); + }); + } + }, _('Save log')); + var nodes = [ statusEl ]; + if (actionHint(action)) + nodes.push(hintEl); + nodes.push(ynBox, logEl); + nodes.push(E('div', { 'class': 'right' }, [ + copyBtn, ' ', saveBtn, ' ', abortEl, ' ', closeBtn + ])); + ui.showModal(_('VeraCrypt'), nodes); + paint(); + var iv = window.setInterval(function() { + elapsed++; + left--; + paint(); + }, 1000); + return invokeRun(opts).then(function(res) { + if (ctl.stopped) + return { ok: false, aborted: true }; + if (res && res.pending) { + window.clearInterval(iv); + return waitJob(left, statusEl, logEl, ctl, action); + } + return res; + }).then(function(res) { + window.clearInterval(iv); + if (ctl.stopped) + return res; + if (res && (res.output || res.error)) + setLogText(logEl, res.output || res.error); + if (res && res.ok === false) { + statusEl.textContent = res.error || _('Failed.'); + closeBtn.style.display = ''; + abortEl.style.display = 'none'; + if (!res) + res = { ok: false }; + res.keepOpen = true; + return res; + } + if (action === 'fsck') { + statusEl.textContent = _('fsck finished. Volume is still decrypted. Close to unmount.'); + closeBtn.style.display = ''; + abortEl.style.display = 'none'; + if (!res) + res = { ok: true }; + res.keepOpen = true; + return res; + } + if (autoClose) { + ui.hideModal(); + window.location.reload(); + if (!res) + res = { ok: true }; + res.keepOpen = true; + return res; + } + statusEl.textContent = (res && res.summary) || _('Finished.'); + closeBtn.style.display = ''; + abortEl.style.display = 'none'; + if (!res) + res = { ok: true }; + res.keepOpen = true; + return res; + }).catch(function(err) { + window.clearInterval(iv); + if (ctl.stopped) + return; + setLogText(logEl, err.message || String(err)); + statusEl.textContent = err.message || String(err); + closeBtn.style.display = ''; + abortEl.style.display = 'none'; + }); +} + +var HASHES = [ '', 'sha-512', 'sha-256', 'ripemd160', 'whirlpool', 'streebog' ]; +var CIPHERS = [ '', 'AES', 'Serpent', 'Twofish', 'Camellia', 'Kuznyechik', + 'AES-Twofish', 'AES-Twofish-Serpent', 'Serpent-AES', 'Serpent-Twofish-AES', + 'Twofish-Serpent' ]; +var FSTYPES = [ '', 'ext4', 'ext3', 'ext2', 'vfat', 'ntfs', 'exfat', 'none' ]; +var VTYPES = [ '', 'normal', 'hidden' ]; + +function slotSelect(cur) { + var opts = [ [ '', _('(none)') ] ]; + for (var i = 1; i <= 64; i++) + opts.push([ String(i), String(i) ]); + return select(opts, cur || ''); +} + +function sectionName(sid) { + return uci.get('veracrypt', sid, '.name') || sid; +} + +function parseListLine(line) { + var parts = String(line || '').trim().split(/\s+/); + return { + volume: parts[1] || '', + vdev: parts[2] || '', + mountpoint: (parts[3] && parts[3] !== '-') ? parts[3] : '' + }; +} + +function safeAbsPath(p) { + p = String(p || ''); + if (!p) + return ''; + if (p.charAt(0) !== '/') + return ''; + if (/(^|\/)\.\.(\/|$)/.test(p)) + return ''; + if (/[`$;|&<>(){}!*?'"\\\n\r\t]/.test(p)) + return ''; + return p; +} + +function saveFavorite(name, opts) { + opts = opts || {}; + name = String(name || '').trim(); + if (!/^[A-Za-z0-9_]{1,32}$/.test(name)) + return Promise.reject({ message: _('Name must be letters, digits or underscore (e.g. buffalo).') }); + if (uci.get('veracrypt', name)) + return Promise.reject({ message: _('A favorite named “%s” already exists.').format(name) }); + var vol = safeAbsPath(opts.volume); + var mp = safeAbsPath(opts.mountpoint); + var kf = safeAbsPath(opts.keyfiles); + if (!vol) + return Promise.reject({ message: _('Invalid volume path.') }); + uci.add('veracrypt', 'volume', name); + uci.set('veracrypt', name, 'volume', vol); + if (mp) + uci.set('veracrypt', name, 'mountpoint', mp); + var sl = parseInt(opts.slot, 10); + if (sl >= 1 && sl <= 64) + uci.set('veracrypt', name, 'slot', String(sl)); + if (kf) + uci.set('veracrypt', name, 'keyfiles', kf); + uci.set('veracrypt', name, 'nokernelcrypto', '1'); + return uci.save().then(function() { return uci.apply(); }); +} + +function suggestFavName(volume) { + var base = String(volume || '').replace(/\/+$/, '').split('/').pop() || ''; + base = base.replace(/[^A-Za-z0-9_]/g, '_').replace(/^_+|_+$/g, ''); + if (!base) + base = 'vol'; + if (/^[0-9]/.test(base)) + base = 'v_' + base; + return base.substring(0, 32); +} + +function promptAddFavorite(opts) { + var inp = E('input', { + 'type': 'text', + 'class': 'cbi-input-text', + 'placeholder': 'buffalo', + 'style': 'width:100%', + 'value': suggestFavName(opts && opts.volume) + }); + function go() { + var n = String(inp.value || '').trim(); + return saveFavorite(n, opts).then(function() { + ui.hideModal(); + ui.addNotification(null, E('p', _('Saved favorite “%s”.').format(n)), 'info'); + window.location.reload(); + }).catch(function(err) { + ui.addNotification(null, E('p', err.message || String(err)), 'error'); + }); + } + inp.addEventListener('keydown', function(ev) { + if (ev.key === 'Enter' || ev.keyCode === 13) { + ev.preventDefault(); + go(); + } + }); + ui.showModal(_('Add as favorite'), [ + E('p', _('Short name for this mounted container or device. After Save it appears in Favorites. Passwords are not stored.')), + E('p', (opts && opts.volume) || ''), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Name')), + E('div', { 'class': 'cbi-value-field' }, inp) + ]), + E('div', { 'class': 'right' }, [ + E('button', { 'type': 'button', 'class': 'btn', 'click': ui.hideModal }, _('Cancel')), + ' ', + E('button', { + 'type': 'button', + 'class': 'btn cbi-button-apply', + 'title': _('Save this volume in the favorite list.'), + 'click': go + }, _('Save')) + ]) + ]); + window.setTimeout(function() { try { inp.focus(); inp.select(); } catch (e) {} }, 50); +} + +return view.extend({ + load: function() { + return uci.load('veracrypt').then(function() { + if (!uci.get('veracrypt', 'main')) { + uci.add('veracrypt', 'settings', 'main'); + uci.set('veracrypt', 'main', 'timeout', '300'); + } + return callStatus(); + }).then(function(st) { + return [ true, st ]; + }); + }, + + render: function(data) { + var st = data[1] || {}; + var status = {}; + (st.volumes || []).forEach(function(v) { + status[v.name] = v; + }); + var slots = st.slots || []; + var m, s, o; + + var body = E('div'); + var ver = String(st.version || '').replace(/^veracrypt\s*/i, ''); + var titleKids = [ + E('a', { + 'href': 'https://veracrypt.jp/en/Home.html', + 'target': '_blank', + 'rel': 'noopener noreferrer', + 'title': _('VeraCrypt home page') + }, _('VeraCrypt')) + ]; + if (ver) + titleKids.push(' ' + ver); + var favVol = {}; + uci.sections('veracrypt', 'volume', function(s) { + if (s.volume) + favVol[s.volume] = true; + }); + body.appendChild(E('div', { + 'style': 'display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap' + }, [ + E('h2', { 'style': 'margin:0' }, titleKids), + E('div', {}, [ + E('button', { + 'type': 'button', + 'class': 'btn', + 'title': _('Show the VeraCrypt version string (veracrypt --version).'), + 'click': ui.createHandlerFn(this, function() { + return runAction({ action: 'version' }); + }) + }, _('Version')), + ' ', + E('button', { + 'type': 'button', + 'class': 'btn', + 'title': _('Show console help (veracrypt --help).'), + 'click': ui.createHandlerFn(this, function() { + return runAction({ action: 'help' }); + }) + }, _('Help')) + ]) + ])); + body.appendChild(E('p', { 'class': 'cbi-map-descr' }, + _('VeraCrypt is a free open source disk encryption software'))); + body.appendChild(E('hr')); + body.appendChild(E('h3', _('Slots'))); + body.appendChild(E('p', { 'class': 'cbi-map-descr' }, + _('Used slots plus one empty slot. Mount container picks a file; Mount device lists /dev/sd*, nvme, mmc, mapper.'))); + + var table = E('table', { 'class': 'table' }, [ + E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th' }, _('Slot')), + E('th', { 'class': 'th' }, _('Volume')), + E('th', { 'class': 'th' }, _('Actions')) + ]) + ]); + (slots || []).forEach(function(sl) { + if (!sl.used) + return; + var parsed = parseListLine(sl.line); + var acts = [ + E('button', { + 'type': 'button', + 'class': 'btn', + 'title': _('Show properties of the volume in slot %s (veracrypt --volume-properties).').format(String(sl.slot)), + 'click': ui.createHandlerFn(this, function() { + return runAction({ action: 'volume-properties', slot: String(sl.slot) }); + }) + }, _('Properties')), + ' ', + E('button', { + 'type': 'button', + 'class': 'btn cbi-button-remove', + 'title': _('Unmount slot %s (veracrypt --unmount --slot=%s).').format(String(sl.slot), String(sl.slot)), + 'click': ui.createHandlerFn(this, function() { + return runAction({ action: 'unmount', slot: String(sl.slot), volume: parsed.volume, mountpoint: parsed.mountpoint }); + }) + }, _('Unmount')) + ]; + if (parsed.volume && !favVol[parsed.volume]) { + acts.push(' '); + acts.push(E('button', { + 'type': 'button', + 'class': 'btn', + 'title': _('Save this mounted volume in Favorites under a short name. Passwords are not stored.'), + 'click': ui.createHandlerFn(this, function() { + promptAddFavorite({ + volume: parsed.volume, + mountpoint: parsed.mountpoint, + slot: String(sl.slot) + }); + }) + }, _('Add as favorite'))); + } + table.appendChild(E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td' }, String(sl.slot)), + E('td', { 'class': 'td' }, sl.line || ''), + E('td', { 'class': 'td' }, acts) + ])); + }); + + var nextSlot = st.next_slot || 1; + if (nextSlot > 0) { + table.appendChild(E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td' }, String(nextSlot)), + E('td', { 'class': 'td' }, _('(empty)')), + E('td', { 'class': 'td' }, [ + E('button', { + 'type': 'button', + 'class': 'btn cbi-button-apply', + 'title': _('Browse for a container file and mount it in slot %s.').format(String(nextSlot)), + 'click': ui.createHandlerFn(this, function() { + openFilePicker(nextSlot); + }) + }, _('Mount container')), + ' ', + E('button', { + 'type': 'button', + 'class': 'btn', + 'title': _('Pick a block device (/dev/sd*, nvme, mmc, mapper) and mount it in slot %s.').format(String(nextSlot)), + 'click': ui.createHandlerFn(this, function() { + openDevicePicker(nextSlot); + }) + }, _('Mount device')) + ]) + ])); + } + body.appendChild(table); + + body.appendChild(E('p', {}, [ + E('button', { + 'type': 'button', + 'class': 'btn', + 'title': _('Unmount every VeraCrypt volume (veracrypt --unmount).'), + 'click': ui.createHandlerFn(this, function() { + return runAction({ action: 'unmount' }); + }) + }, _('Unmount all')) + ])); + + function actionModal(title, extraNodes, collect, initial) { + initial = initial || {}; + var showVolume = !initial.hideVolume; + var showFilename = !!initial.showFilename; + var showMount = !!initial.showMount; + var showKeyfiles = !!initial.showKeyfiles; + var showSlot = !!initial.showSlot; + var showQuick = !!initial.showQuick; + var showForce = !!initial.showForce; + var vol = pathRow(_('Volume / file'), initial.volume || '', !!initial.dirOnly); + var mp = pathRow(_('Mount point'), initial.mountpoint || '', true); + var fname = field('text', { 'placeholder': initial.filenamePlaceholder || 'media.hc', 'value': initial.filename || '' }); + var kf = keyfilesRow(_('Keyfiles')); + var nkf = keyfilesRow(_('New keyfiles')); + var rnd = field('text', { 'value': '/dev/urandom', 'placeholder': '/dev/urandom' }); + var pw = field('password', { 'placeholder': _('Enter password') }); + var npw = field('password', { 'placeholder': _('Enter password') }); + var pim = field('text', { 'placeholder': '0' }); + var npim = field('text'); + var slot = slotSelect(initial.slot || ''); + var hash = select(HASHES, initial.hash || ''); + var nhash = select(HASHES, ''); + var enc = select(CIPHERS, initial.encryption || ''); + var fs = select(FSTYPES, initial.filesystem || ''); + var vtype = select(VTYPES, initial.volume_type || 'normal'); + var size = field('text', { 'placeholder': '100M' }); + var autom = select([ + [ 'favorites', _('favorites') ], + [ 'devices', _('devices') ], + [ 'devices_favorites', _('devices and favorites') ] + ], 'favorites'); + var force = field('checkbox'); + var quick = field('checkbox'); + var fsckauto = field('checkbox'); + var bak = pathRow(_('Header backup file'), initial.backup_file || '', false); + var hpw = field('password'); + var fav = field('checkbox'); + var favname = field('text', { 'placeholder': 'buffalo' }); + quick.checked = !!showQuick; + fsckauto.checked = true; + if (initial.size) + size.value = initial.size; + + var nodes = [ + E('p', _('Passwords are sent on stdin and are not saved.')) + ]; + if (showVolume) + nodes.push(vol.node); + if (showFilename) + nodes.push(E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Container file name')), + E('div', { 'class': 'cbi-value-field' }, fname) + ])); + if (showMount) + nodes.push(mp.node); + if (showKeyfiles) + nodes.push(kf.node); + nodes = nodes.concat(extraNodes({ + vol: vol, mp: mp, kf: kf, nkf: nkf, rnd: rnd, pw: pw, npw: npw, + pim: pim, npim: npim, slot: slot, hash: hash, nhash: nhash, + enc: enc, fs: fs, vtype: vtype, size: size, autom: autom, + force: force, quick: quick, fname: fname, fsckauto: fsckauto, + bak: bak, hpw: hpw, fav: fav, favname: favname + })); + if (showSlot) + nodes.push(E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Slot (1–64)')), + E('div', { 'class': 'cbi-value-field' }, slot) + ])); + if (showQuick || showForce) + nodes.push(E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Options')), + E('div', { 'class': 'cbi-value-field' }, [ + showQuick ? E('label', {}, [ quick, ' ', _('quick format') ]) : '', + showQuick && showForce ? ' ' : '', + showForce ? E('label', {}, [ force, ' ', _('overwrite if the file exists') ]) : '' + ]) + ])); + nodes.push(E('div', { 'class': 'right' }, [ + E('button', { + 'type': 'button', + 'class': 'btn', + 'title': _('Close this dialog without running VeraCrypt.'), + 'click': ui.hideModal + }, _('Cancel')), + ' ', + E('button', { + 'type': 'button', + 'class': 'btn cbi-button-apply', + 'title': _('Run the VeraCrypt command. The password is sent on stdin, not --password on the command line.'), + 'click': ui.createHandlerFn(this, function() { + var o = collect({ + vol: vol, mp: mp, kf: kf, nkf: nkf, rnd: rnd, pw: pw, npw: npw, + pim: pim, npim: npim, slot: slot, hash: hash, nhash: nhash, + enc: enc, fs: fs, vtype: vtype, size: size, autom: autom, + force: force, quick: quick, fname: fname, fsckauto: fsckauto, + bak: bak, hpw: hpw, fav: fav, favname: favname + }); + if (showVolume) + o.volume = vol.getValue(); + if (showMount) + o.mountpoint = mp.getValue(); + if (showKeyfiles) + o.keyfiles = kf.getValue(); + if (showSlot) + o.slot = val(slot); + if (showQuick) + o.quick = flag(quick); + if (showForce) + o.force = flag(force); + if (o.action === 'create' || o.action === 'create-keyfile') { + var fn = val(fname) || initial.filename || (o.action === 'create-keyfile' ? 'keyfile' : 'media.hc'); + o.volume = String((showVolume ? vol.getValue() : '') || '/mnt').replace(/\/+$/, '') + '/' + fn.replace(/^\/+/, ''); + if (o.action === 'create-keyfile') { + o.random_source = val(rnd) || '/dev/urandom'; + } + } + if (o.action === 'create') { + o.size = val(size) || '100M'; + o.encryption = val(enc) || 'AES-Twofish-Serpent'; + o.hash = val(hash) || 'sha-512'; + o.volume_type = val(vtype) || 'normal'; + o.filesystem = val(fs) || 'none'; + o.pim = val(pim); + o.password = val(pw); + o.random_source = val(rnd) || '/dev/urandom'; + o.slot = ''; + o.mount_options = ''; + } + if (o.action === 'mount') { + o.password = val(pw); + o.pim = val(pim) || '0'; + o.protect_hidden = 'no'; + o.mount_options = 'nokernelcrypto'; + o.save_favorite = flag(fav); + o.favorite_name = val(favname) || suggestFavName(o.volume); + } + if (o.action === 'fsck') { + o.password = val(pw); + o.pim = val(pim) || '0'; + o.filesystem = val(fs); + o.fsck_auto = flag(fsckauto) ? '1' : '0'; + o.protect_hidden = 'no'; + o.mount_options = 'nokernelcrypto'; + } + if (o.action === 'backup-headers' || o.action === 'restore-headers') { + o.password = val(pw); + o.pim = val(pim); + o.backup_file = bak.getValue(); + o.protection_password = val(hpw); + o.random_source = '/dev/urandom'; + } + ui.hideModal(); + return ensureFsPackages(o).then(function(go) { + if (!go) + return; + return ensureFsckPackages(o).then(function(go2) { + if (!go2) + return; + return runAction(o).then(function(res) { + var next = Promise.resolve(res); + if (o.save_favorite && res && res.ok !== false && !res.aborted) { + next = saveFavorite(o.favorite_name, { + volume: o.volume, + mountpoint: o.mountpoint, + slot: o.slot, + keyfiles: o.keyfiles + }).catch(function(err) { + ui.addNotification(null, E('p', err.message || String(err)), 'error'); + }).then(function() { return res; }); + } + return next.then(function(r) { + if (r && r.keepOpen) + return; + if (r && r.ok !== false && (o.action === 'mount' || o.action === 'create')) + window.location.reload(); + }); + }); + }); + }); + }) + }, _('Run')) + ])); + ui.showModal(title, [ + E('form', { + 'submit': function(ev) { + if (ev && ev.preventDefault) + ev.preventDefault(); + return false; + } + }, nodes) + ]); + } + + function mountExtras(f) { + var nodes = [ + E('p', _('Cipher and hash come from the volume header. Password is required. PIM and keyfiles only if the volume was created with them. Always mounts with nokernelcrypto.')), + E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('Password')), E('div', { 'class': 'cbi-value-field' }, [ + E('p', { 'class': 'cbi-map-descr' }, _('Enter password')), + f.pw + ]) ]), + E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('PIM (empty = VeraCrypt default)')), E('div', { 'class': 'cbi-value-field' }, f.pim) ]) + ]; + var volPath = f.vol ? f.vol.getValue() : ''; + if (!volPath || !favVol[volPath]) { + nodes.push(E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Add as favorite')), + E('div', { 'class': 'cbi-value-field' }, [ + E('label', {}, [ f.fav, ' ', _('After a successful mount, save this volume in Favorites (passwords are not stored).') ]), + E('p', { 'class': 'cbi-map-descr' }, _('Favorite name (letters, digits, underscore)')), + f.favname + ]) + ])); + } + return nodes; + } + + function openFsck(initial) { + actionModal(_('Check filesystem'), function(f) { + return [ + E('p', _('Decrypts without mounting (veracrypt --filesystem=none), lists the mapper or loop device (veracrypt -l), runs fsck -f on that device, then dismounts. Unmount the volume first if it is mounted. Default is automatic yes to all prompts; uncheck for interactive y/n.')), + E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('Password')), E('div', { 'class': 'cbi-value-field' }, [ + E('p', { 'class': 'cbi-map-descr' }, _('Enter password')), + f.pw + ]) ]), + E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('PIM (empty = default)')), E('div', { 'class': 'cbi-value-field' }, f.pim) ]), + E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('Inner filesystem (optional hint)')), E('div', { 'class': 'cbi-value-field' }, f.fs) ]), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Automatic yes')), + E('div', { 'class': 'cbi-value-field' }, [ + E('label', {}, [ f.fsckauto, ' ', _('Yes to all fsck prompts (default). Uncheck to answer y or n.') ]) + ]) + ]) + ]; + }, function() { return { action: 'fsck' }; }, { + volume: (initial && initial.volume) || '', + slot: (initial && initial.slot) || '', + showKeyfiles: true, + showSlot: !!(initial && initial.slot) + }); + } + + function openFilePicker(slotNo) { + var row = pathRow(_('Container file'), '', false); + ui.showModal(_('Mount container'), [ + E('p', _('Browse, create or delete, then select the container. Create and delete stay in this dialog.')), + row.node, + E('div', { 'class': 'right' }, [ + E('button', { + 'type': 'button', + 'class': 'btn', + 'title': _('Close the file picker without mounting.'), + 'click': ui.hideModal + }, _('Cancel')), + ' ', + E('button', { + 'type': 'button', + 'class': 'btn cbi-button-apply', + 'title': _('Use the selected container file and open the mount dialog for slot %s.').format(String(slotNo)), + 'click': function() { + var p = row.getValue(); + ui.hideModal(); + if (!p) + return; + actionModal(_('Mount'), mountExtras, function() { + return { action: 'mount' }; + }, { volume: p, slot: String(slotNo), showMount: true, showKeyfiles: true, showSlot: true }); + } + }, _('Use file')) + ]) + ]); + } + + function openDevicePicker(slotNo) { + function usePath(p) { + ui.hideModal(); + actionModal(_('Mount'), mountExtras, function() { + return { action: 'mount' }; + }, { volume: p, slot: String(slotNo), showMount: true, showKeyfiles: true, showSlot: true }); + } + ui.showModal(_('Mount device'), [ E('p', _('Loading block devices…')) ]); + return callListDev().then(function(res) { + var devs = (res && res.devices) || []; + var rows = [ E('p', _('Block devices (/dev/sd*, nvme, mmc, mapper, and other /sys/class/block nodes)')) ]; + if (!devs.length) + rows.push(E('p', _('No nodes under /sys/class/block. You can still browse /dev.'))); + devs.forEach(function(d) { + rows.push(E('div', {}, [ + E('button', { + 'type': 'button', + 'class': 'btn', + 'style': 'margin:2px', + 'title': _('Mount %s as a VeraCrypt volume in slot %s.').format(d.path, String(slotNo)), + 'click': function() { usePath(d.path); } + }, d.path) + ])); + }); + var fu = new ui.FileUpload('/dev', { + root_directory: '/dev', + initial_directory: '/dev', + show_hidden: true, + enable_upload: false, + enable_remove: false, + enable_download: false, + directory_create: false, + directory_select: false + }); + rows.push(E('p', _('Or browse /dev:'))); + var holder = E('div'); + rows.push(holder); + Promise.resolve(fu.render()).then(function(el) { holder.appendChild(el); }); + rows.push(E('div', { 'class': 'right' }, [ + E('button', { + 'type': 'button', + 'class': 'btn', + 'title': _('Close the device picker without mounting.'), + 'click': ui.hideModal + }, _('Cancel')), + ' ', + E('button', { + 'type': 'button', + 'class': 'btn cbi-button-apply', + 'title': _('Use the selected /dev node and open the mount dialog for slot %s.').format(String(slotNo)), + 'click': function() { + var p = fu.getValue(); + if (p) + usePath(p); + } + }, _('Use selected /dev node')) + ])); + ui.showModal(_('Mount device'), rows); + }).catch(function(err) { + ui.hideModal(); + ui.addNotification(null, E('p', err.message || String(err)), 'error'); + }); + } + + body.appendChild(E('h3', _('Operations'))); + body.appendChild(E('p', {}, [ + E('button', { + 'type': 'button', + 'class': 'btn', + 'title': _('Check the inner filesystem: decrypt with --filesystem=none, run fsck on the mapper or loop device, then dismount. Unmount first if the volume is mounted.'), + 'click': function() { + openFsck({ slot: String(nextSlot || 1) }); + } }, _('Check filesystem…')), + ' ', + E('button', { + 'type': 'button', + 'class': 'btn', + 'title': _('Create a new volume (folder + file name, size, password, mount directory). Creating a mount directory does not close this dialog. After create the volume is mounted there.'), + 'click': function() { + actionModal(_('Create volume'), function(f) { + return [ + E('p', _('Folder + file name become the container path. Set or create the mount directory; creating a directory does not close this dialog. After create, the volume is mounted there. Defaults: AES-Twofish-Serpent, SHA-512, 100M, quick format.')), + E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('Password')), E('div', { 'class': 'cbi-value-field' }, [ + E('p', { 'class': 'cbi-map-descr' }, _('Enter password')), + f.pw + ]) ]), + E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('PIM (empty = VeraCrypt default)')), E('div', { 'class': 'cbi-value-field' }, f.pim) ]), + E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('Size (--size)')), E('div', { 'class': 'cbi-value-field' }, f.size) ]), + E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('Volume type')), E('div', { 'class': 'cbi-value-field' }, f.vtype) ]), + E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('Encryption')), E('div', { 'class': 'cbi-value-field' }, f.enc) ]), + E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('Hash')), E('div', { 'class': 'cbi-value-field' }, f.hash) ]), + E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('Inner filesystem')), E('div', { 'class': 'cbi-value-field' }, f.fs) ]) + ]; + }, function() { return { action: 'create' }; }, { + encryption: 'AES-Twofish-Serpent', + hash: 'sha-512', + volume_type: 'normal', + filesystem: 'ext4', + dirOnly: true, + showMount: true, + showFilename: true, + showQuick: true, + showForce: true, + volume: '/mnt', + mountpoint: '/mnt/Buffalo', + filename: 'media.hc', + size: '100M' + }); + } }, _('Create…')), + ' ', + E('button', { + 'type': 'button', + 'class': 'btn', + 'title': _('Change the volume password and/or keyfiles (veracrypt --change). Current password is sent on stdin.'), + 'click': function() { + actionModal(_('Change password / keyfiles'), function(f) { + return [ + E('p', _('veracrypt --change. Current and new password are both fed on stdin; neither --password nor --new-password appears on the command line.')), + E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('Current password')), E('div', { 'class': 'cbi-value-field' }, f.pw) ]), + E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('New password')), E('div', { 'class': 'cbi-value-field' }, f.npw) ]), + E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('PIM / new PIM')), E('div', { 'class': 'cbi-value-field' }, [ f.pim, f.npim ]) ]), + E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('Hash / new hash')), E('div', { 'class': 'cbi-value-field' }, [ f.hash, f.nhash ]) ]), + f.nkf.node + ]; + }, function(f) { + return { + action: 'change', + password: val(f.pw), + new_password: val(f.npw), + pim: val(f.pim), + new_pim: val(f.npim), + hash: val(f.hash), + new_hash: val(f.nhash), + new_keyfiles: f.nkf.getValue() + }; + }, { showKeyfiles: true }); + } }, _('Change…')), + ' ', + E('button', { + 'type': 'button', + 'class': 'btn', + 'title': _('Write a backup of the volume headers (veracrypt --backup-headers).'), + 'click': function() { + actionModal(_('Backup headers'), function(f) { + return [ + E('p', _('Writes an external header backup to the file below (not inside the volume). Enter the outer-volume password. If there is a hidden volume, also enter its password. VeraCrypt CLI has no --backup-file flag; this dialog feeds the path and passwords on stdin.')), + E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('Outer volume password')), E('div', { 'class': 'cbi-value-field' }, f.pw) ]), + E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('PIM (empty = 0)')), E('div', { 'class': 'cbi-value-field' }, f.pim) ]), + E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('Hidden volume password (empty = none)')), E('div', { 'class': 'cbi-value-field' }, f.hpw) ]), + f.bak.node + ]; + }, function(f) { + return { action: 'backup-headers' }; + }, { showKeyfiles: true, backup_file: '/mnt/volume.header.bak' }); + } }, _('Backup headers…')), + ' ', + E('button', { + 'type': 'button', + 'class': 'btn', + 'title': _('Restore volume headers from a backup (veracrypt --restore-headers).'), + 'click': function() { + actionModal(_('Restore headers'), function(f) { + return [ + E('p', _('Restores headers from an external backup file into the volume. Volume / file is the container to repair. Header backup file is the .header.bak (or similar) created by Backup headers.')), + E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('Password for the backup')), E('div', { 'class': 'cbi-value-field' }, f.pw) ]), + E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('PIM (empty = 0)')), E('div', { 'class': 'cbi-value-field' }, f.pim) ]), + f.bak.node + ]; + }, function(f) { + return { action: 'restore-headers' }; + }, { showKeyfiles: true, backup_file: '/mnt/volume.header.bak' }); + } }, _('Restore headers…')), + ' ', + E('button', { + 'type': 'button', + 'class': 'btn', + 'title': _('Create a new random keyfile as a named file. VeraCrypt never modifies keyfile contents.'), + 'click': function() { + actionModal(_('Create keyfile'), function(f) { + return [ + E('p', _('Choose the directory, then type the file name to create. veracrypt --create-keyfile writes that file. A directory cannot be the keyfile path.')), + E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('Random source')), E('div', { 'class': 'cbi-value-field' }, f.rnd) ]) + ]; + }, function(f) { + return { action: 'create-keyfile', random_source: val(f.rnd) || '/dev/urandom' }; + }, { + dirOnly: true, + showFilename: true, + filename: 'keyfile', + filenamePlaceholder: 'keyfile', + volume: '/mnt' + }); + } }, _('Create keyfile…')), + ' ', + E('button', { + 'type': 'button', + 'class': 'btn', + 'title': _('Mount favorite or device-hosted volumes (veracrypt --auto-mount).'), + 'click': function() { + actionModal(_('Auto-mount'), function(f) { + return [ + E('p', _('veracrypt --auto-mount=favorites|devices|devices_favorites. Always nokernelcrypto.')), + E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('What to mount')), E('div', { 'class': 'cbi-value-field' }, f.autom) ]), + E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('Password')), E('div', { 'class': 'cbi-value-field' }, [ + E('p', { 'class': 'cbi-map-descr' }, _('Enter password')), + f.pw + ]) ]), + E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('PIM (empty = default)')), E('div', { 'class': 'cbi-value-field' }, f.pim) ]) + ]; + }, function(f) { + return { + action: 'auto-mount', + auto_mount: val(f.autom) || 'favorites', + password: val(f.pw), + pim: val(f.pim), + mount_options: 'nokernelcrypto' + }; + }, { hideVolume: true, showKeyfiles: true }); + } }, _('Auto-mount…')), + ' ', + E('button', { + 'type': 'button', + 'class': 'btn', + 'title': _('List keyfiles on the PKCS #11 token. Set Settings → Security token library first.'), + 'click': function() { + var lib = uci.get('veracrypt', 'main', 'token_lib') || ''; + if (!lib) { + ui.addNotification(null, E('p', + _('No PKCS #11 library path is set. Use Settings → Security token library (example: /usr/lib/libykcs11.so).') + ), 'warning'); + return; + } + return runAction({ action: 'list-token-keyfiles', token_lib: lib }); + } }, _('List token keyfiles')), + ' ', + E('button', { + 'type': 'button', + 'class': 'btn', + 'title': _('Run VeraCrypt algorithm self-tests (veracrypt --test). Uses the Settings timeout (minimum 300 seconds).'), + 'click': function() { + return runAction({ action: 'test' }); + } + }, _('Test algorithms')) + ])); + + body.appendChild(E('hr')); + m = new form.Map('veracrypt'); + + s = m.section(form.NamedSection, 'main', 'settings', _('Settings')); + s.addremove = false; + s.anonymous = false; + s.description = _('Timeouts for mount, create, fsck and algorithm tests. PKCS #11 is optional. Save & Apply after changes.'); + o = s.option(form.Value, 'timeout', _('XHR / operation timeout (seconds)')); + o.datatype = 'and(uinteger,min(300))'; + o.placeholder = '300'; + o.default = '300'; + o.description = _('Minimum 300 seconds. Raise this for slow devices, large volumes, or full (non-quick) format. Each long operation shows its own elapsed time and remaining timeout.'); + + o = s.option(form.FileUpload, 'token_lib', _('Security token library (PKCS #11)')); + o.root_directory = '/'; + o.show_hidden = true; + o.enable_upload = false; + o.enable_remove = false; + o.optional = true; + o.description = _('Optional. Path to a PKCS #11 .so (for example /usr/lib/libykcs11.so). Leave empty if you do not use a token.'); + + var mFav = new form.Map('veracrypt'); + s = mFav.section(form.GridSection, 'volume', _('Favorites'), + _('Add a mounted volume from its slot row (Add as favorite), or tick Add as favorite in the mount dialog. Delete from list removes the saved name only — it does not unmount or delete the container. Passwords are not stored. Slot is 1–64.')); + s.anonymous = false; + s.addremove = false; + s.nodescriptions = true; + s.modaltitle = function(sid) { + return _('Favorite “%s”: choose volume and mount point').format(sid); + }; + + o = s.option(form.DummyValue, '_state', _('State')); + o.modalonly = false; + o.textvalue = function(sid) { + var stv = status[sectionName(sid)]; + return stv && stv.mounted ? _('Mounted') : _('Dismounted'); + }; + + o = s.option(form.FileUpload, 'volume', _('Volume file')); + o.root_directory = '/'; + o.show_hidden = true; + o.enable_upload = false; + o.enable_remove = false; + o.enable_download = false; + o.directory_create = true; + o.rmempty = false; + o.editable = true; + + o = s.option(form.FileUpload, 'mountpoint', _('Mount point')); + o.root_directory = '/'; + o.show_hidden = true; + o.enable_upload = false; + o.enable_remove = false; + o.directory_create = true; + o.directory_select = true; + o.rmempty = false; + o.editable = true; + + o = s.option(form.ListValue, 'slot', _('Slot')); + o.value('', _('(auto)')); + for (var i = 1; i <= 64; i++) + o.value(String(i), String(i)); + o.modalonly = true; + + o = s.option(form.Flag, 'nokernelcrypto', _('No kernel crypto')); + o.default = '1'; + o.modalonly = true; + o.description = _('Always recommended on this router (veracrypt -m=nokernelcrypto).'); + + o = s.option(form.Value, 'pim', _('PIM')); + o.datatype = 'uinteger'; + o.placeholder = '0'; + o.modalonly = true; + + o = s.option(form.ListValue, 'protect_hidden', _('Protect hidden volume')); + o.value('no', _('No')); + o.value('yes', _('Yes')); + o.default = 'no'; + o.modalonly = true; + + o = s.option(form.FileUpload, 'keyfiles', _('Keyfiles')); + o.root_directory = '/'; + o.show_hidden = true; + o.enable_upload = false; + o.enable_remove = false; + o.directory_create = true; + o.directory_select = true; + o.modalonly = true; + + o = s.option(form.Flag, 'truecrypt', _('TrueCrypt mode')); + o.modalonly = true; + o.description = _('Only if the container is a TrueCrypt volume.'); + + o = s.option(form.DummyValue, '_actions', _('Actions')); + o.modalonly = false; + o.rawhtml = true; + o.textvalue = function(sid) { + var name = sectionName(sid); + var stv = status[name]; + var mounted = stv && stv.mounted; + var wrap = E('span', { 'style': 'white-space:nowrap' }); + if (mounted) { + wrap.appendChild(E('button', { + 'type': 'button', + 'class': 'btn', + 'title': _('Show properties of this mounted volume (veracrypt --volume-properties).'), + 'click': ui.createHandlerFn(this, function() { + return runAction({ + action: 'volume-properties', + volume: uci.get('veracrypt', sid, 'volume') || '', + slot: uci.get('veracrypt', sid, 'slot') || '' + }); + }) + }, _('Properties'))); + wrap.appendChild(E('span', {}, ' ')); + wrap.appendChild(E('button', { + 'type': 'button', + 'class': 'btn cbi-button-remove', + 'title': _('Unmount favorite %s from its mount point.').format(name), + 'click': ui.createHandlerFn(this, function() { + return runAction({ + action: 'unmount', + name: name, + volume: uci.get('veracrypt', sid, 'volume') || '', + mountpoint: uci.get('veracrypt', sid, 'mountpoint') || '', + slot: uci.get('veracrypt', sid, 'slot') || '' + }); + }) + }, _('Unmount'))); + wrap.appendChild(E('span', {}, ' ')); + } + else { + wrap.appendChild(E('button', { + 'type': 'button', + 'class': 'btn cbi-button-apply', + 'title': _('Mount favorite %s. Password is sent on stdin, not --password.').format(name), + 'click': ui.createHandlerFn(this, function() { + var pw = field('password', { 'placeholder': _('Enter password') }); + ui.showModal(_('Mount %s').format(name), [ + E('p', uci.get('veracrypt', sid, 'volume') || ''), + E('label', {}, _('Enter password')), + pw, + E('div', { 'class': 'right' }, [ + E('button', { + 'type': 'button', + 'class': 'btn', + 'title': _('Close without mounting.'), + 'click': ui.hideModal + }, _('Cancel')), + ' ', + E('button', { + 'type': 'button', + 'class': 'btn cbi-button-apply', + 'title': _('Mount this favorite. Password is sent on stdin.'), + 'click': ui.createHandlerFn(this, function() { + ui.hideModal(); + return runAction({ action: 'mount', name: name, password: val(pw) }).then(function(res) { + if (res && res.ok !== false) + window.location.reload(); + }); + }) + }, _('Mount')) + ]) + ]); + }) + }, _('Mount'))); + wrap.appendChild(E('span', {}, ' ')); + wrap.appendChild(E('button', { + 'type': 'button', + 'class': 'btn', + 'title': _('Check the inner filesystem of this favorite: decrypt with --filesystem=none, fsck, then dismount.'), + 'click': ui.createHandlerFn(this, function() { + openFsck({ + volume: uci.get('veracrypt', sid, 'volume') || '', + slot: uci.get('veracrypt', sid, 'slot') || '' + }); + }) + }, _('Check'))); + wrap.appendChild(E('span', {}, ' ')); + } + wrap.appendChild(E('button', { + 'type': 'button', + 'class': 'btn cbi-button-remove', + 'title': _('Remove “%s” from the favorite list. Does not unmount or delete the container file.').format(name), + 'click': ui.createHandlerFn(this, function() { + if (!window.confirm(_('Remove “%s” from favorites? The volume file is not deleted and a mounted volume stays mounted.').format(name))) + return; + uci.remove('veracrypt', sid); + return uci.save().then(function() { return uci.apply(); }).then(function() { + window.location.reload(); + }); + }) + }, _('Delete from list'))); + return wrap; + }; + + return m.render().then(function(node) { + body.appendChild(node); + body.appendChild(E('hr')); + return mFav.render(); + }).then(function(node) { + body.appendChild(node); + return body; + }); + } +}); diff --git a/applications/luci-app-veracrypt/root/etc/config/veracrypt b/applications/luci-app-veracrypt/root/etc/config/veracrypt new file mode 100644 index 000000000000..913f8bab7005 --- /dev/null +++ b/applications/luci-app-veracrypt/root/etc/config/veracrypt @@ -0,0 +1,10 @@ +config settings 'main' + option timeout '300' + option token_lib '' + +# config volume 'media' +# option volume '/mnt/sda2/media.tc' +# option mountpoint '/mnt/Buffalo' +# option nokernelcrypto '1' +# option pim '0' +# option protect_hidden 'no' diff --git a/applications/luci-app-veracrypt/root/usr/libexec/rpcd/luci.veracrypt b/applications/luci-app-veracrypt/root/usr/libexec/rpcd/luci.veracrypt new file mode 100755 index 000000000000..74d95a58e13a --- /dev/null +++ b/applications/luci-app-veracrypt/root/usr/libexec/rpcd/luci.veracrypt @@ -0,0 +1,1730 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0-only +# rpcd backend for luci-app-veracrypt (veracrypt --text --non-interactive) + +. /usr/share/libubox/jshn.sh + +VC=/usr/bin/veracrypt +JOB=/tmp/luci-veracrypt-job + +valid_name() { + [ -z "$1" ] && return 1 + LC_ALL=C echo "$1" | grep -q '^[A-Za-z0-9_]\{1,32\}$' +} + +# Absolute paths only. Deny shell metacharacters (values are always quoted). +# Passwords are not checked here. Spaces and dots in names are allowed. +path_has_metas() { + case $1 in *'`'*) return 0 ;; esac + case $1 in *'$'*) return 0 ;; esac + case $1 in *';'*) return 0 ;; esac + case $1 in *'|'*) return 0 ;; esac + case $1 in *'&'*) return 0 ;; esac + case $1 in *'<'*|*'>'*) return 0 ;; esac + case $1 in *'('*|*')'*) return 0 ;; esac + case $1 in *'{'*|*'}'*) return 0 ;; esac + case $1 in *'!'*) return 0 ;; esac + case $1 in *'*'*) return 0 ;; esac + case $1 in *'?'*) return 0 ;; esac + case $1 in *"'"*) return 0 ;; esac + case $1 in *'"'*) return 0 ;; esac + case $1 in *'\\'*) return 0 ;; esac + return 1 +} + +valid_path() { + [ -z "$1" ] && return 0 + case $1 in + /*) ;; + *) return 1 ;; + esac + case "/$1/" in + */../*) return 1 ;; + esac + path_has_metas "$1" && return 1 + # $(printf '\n') is empty (command substitution strips newlines). + stripped=$(printf '%s' "$1" | tr -d '\n\r\t') + [ "$stripped" = "$1" ] || return 1 + return 0 +} + +# Absolute file/dir, token://slot/N/file/NAME, emv://slot/N, comma-separated list. +# Double comma (,,) is a literal comma in a name (VeraCrypt --keyfiles). +valid_keyfiles() { + [ -z "$1" ] && return 0 + path_has_metas "$1" && return 1 + stripped=$(printf '%s' "$1" | tr -d '\n\r\t') + [ "$stripped" = "$1" ] || return 1 + case $1 in + /*|token://slot/*|emv://slot/*) ;; + *) return 1 ;; + esac + return 0 +} + +valid_slot() { + [ -z "$1" ] && return 0 + echo "$1" | grep -q '^[1-9][0-9]*$' || return 1 + [ "$1" -ge 1 ] && [ "$1" -le 64 ] +} + +valid_action() { + case $1 in + mount|unmount|create|fsck|change|list|test|version|help|\ + backup-headers|restore-headers|create-keyfile|volume-properties|\ + auto-mount|list-token-keyfiles) return 0 ;; + *) return 1 ;; + esac +} + +valid_hash() { + [ -z "$1" ] && return 0 + case $1 in sha-512|sha-256|ripemd160|whirlpool|streebog) return 0 ;; *) return 1 ;; esac +} + +valid_encryption() { + [ -z "$1" ] && return 0 + case $1 in + AES|Serpent|Twofish|Camellia|Kuznyechik|\ + AES-Twofish|AES-Twofish-Serpent|Serpent-AES|Serpent-Twofish-AES|Twofish-Serpent) + return 0 ;; + *) return 1 ;; + esac +} + +valid_filesystem() { + [ -z "$1" ] && return 0 + case $1 in ext4|ext3|ext2|vfat|ntfs|exfat|none|fat|fat32|msdos) return 0 ;; *) return 1 ;; esac +} + +valid_volume_type() { + [ -z "$1" ] && return 0 + case $1 in normal|hidden) return 0 ;; *) return 1 ;; esac +} + +valid_protect_hidden() { + [ -z "$1" ] && return 0 + case $1 in no|yes) return 0 ;; *) return 1 ;; esac +} + +valid_auto_mount() { + [ -z "$1" ] && return 0 + case $1 in favorites|devices|devices_favorites) return 0 ;; *) return 1 ;; esac +} + +valid_pim() { + [ -z "$1" ] && return 0 + echo "$1" | grep -q '^[0-9]\{1,7\}$' || return 1 + [ "$1" -le 2147468 ] +} + +valid_size() { + [ -z "$1" ] && return 0 + echo "$1" | grep -q '^[1-9][0-9]*[KMGTPkmgtp]\?$' +} + +valid_flag() { + [ -z "$1" ] && return 0 + [ "$1" = 1 ] || [ "$1" = 0 ] +} + +valid_mount_options() { + [ -z "$1" ] && return 0 + [ "$1" = nokernelcrypto ] +} + +valid_random() { + [ -z "$1" ] && return 0 + case $1 in /dev/urandom|/dev/random) return 0 ;; *) return 1 ;; esac +} + +valid_vdev() { + case $1 in + /dev/loop[0-9]|/dev/loop[0-9][0-9]|/dev/mapper/*|/dev/dm-[0-9]*) return 0 ;; + *) return 1 ;; + esac +} + +valid_fs_options() { + [ -z "$1" ] && return 0 + LC_ALL=C printf '%s' "$1" | grep -q '[^a-z0-9,=_-]' && return 1 + return 0 +} + +json_fail() { + json_init + json_add_boolean ok 0 + json_add_string error "$1" + json_dump +} + +json_ok() { + json_init + json_add_boolean ok 1 + [ -n "$1" ] && json_add_string output "$1" + json_dump +} + +read_json() { + # ubus does not always send a trailing newline; read -r would hang. + input=$(cat) + [ -n "$input" ] || input='{}' + json_load "$input" +} + +have_bin() { + n=$1 + command -v "$n" >/dev/null 2>&1 && return 0 + [ -x "/sbin/$n" ] && return 0 + [ -x "/usr/sbin/$n" ] && return 0 + [ -x "/usr/bin/$n" ] && return 0 + [ -x "/bin/$n" ] && return 0 + return 1 +} + +# Filesystem usable: loaded, listed, module present, or apk package installed. +have_fs() { + mod=$1 + pkg=$2 + grep -qw "$mod" /proc/filesystems 2>/dev/null && return 0 + [ -d "/sys/module/$mod" ] && return 0 + kr=$(uname -r 2>/dev/null) + if [ -n "$kr" ]; then + ls "/lib/modules/$kr" 2>/dev/null | grep -q "$mod" && return 0 + fi + if [ -n "$pkg" ]; then + ab=$(apk_bin) + [ -n "$ab" ] && "$ab" info -e "$pkg" >/dev/null 2>&1 && return 0 + fi + return 1 +} + +apk_bin() { + if [ -x /usr/bin/apk ]; then echo /usr/bin/apk + elif [ -x /bin/apk ]; then echo /bin/apk + elif [ -x /sbin/apk ]; then echo /sbin/apk + else echo "" + fi +} + +# Password on stdin (--stdin), never --password / -p (visible in ps). +# printf %s, not an unquoted here-doc: $(...) in a password must not run as shell. +vc_invoke() { + _pw=$1 + shift + if [ -n "$_pw" ]; then + printf '%s\n' "$_pw" | "$VC" --stdin "$@" + else + "$VC" "$@" + fi +} + +# Extra secrets (new password, hidden-volume password, token PIN) also stay +# off argv. Drop --non-interactive/--stdin so VeraCrypt reads them as prompts. +vc_invoke_prompts() { + _pw=$1 + _prot=$2 + _pin=$3 + shift 3 + _n=0 + for _a in "$@"; do + case $_a in + --non-interactive|--stdin) continue ;; + esac + _n=$((_n + 1)) + eval "_p_$_n=\$_a" + done + set -- + _i=1 + while [ "$_i" -le "$_n" ]; do + eval "set -- \"\$@\" \"\$_p_$_i\"" + _i=$((_i + 1)) + done + { + [ -n "$_pw" ] && printf '%s\n' "$_pw" + [ -n "$_prot" ] && printf '%s\n' "$_prot" + [ -n "$_pin" ] && printf '%s\n' "$_pin" + } | "$VC" "$@" +} + +vc_tmp() { + mktemp /tmp/vc.XXXXXX 2>/dev/null || echo /dev/null +} + +# Uses the cmd_run password / protection_password / token_pin / protect_hidden vars. +vc_run() { + if [ -n "$token_pin" ] || [ -n "$protection_password" ]; then + _pr= + [ "$protect_hidden" = yes ] && _pr=$protection_password + vc_invoke_prompts "$password" "$_pr" "$token_pin" "$@" + else + vc_invoke "$password" "$@" + fi +} + +cmd_tools() { + json_init + json_add_boolean ok 1 + have_bin dmsetup && json_add_boolean has_dmsetup 1 || json_add_boolean has_dmsetup 0 + have_bin mkfs.ext4 && json_add_boolean has_mkfs_ext4 1 || json_add_boolean has_mkfs_ext4 0 + have_bin mkfs.ext3 && json_add_boolean has_mkfs_ext3 1 || json_add_boolean has_mkfs_ext3 0 + have_bin mkfs.vfat && json_add_boolean has_mkfs_vfat 1 || json_add_boolean has_mkfs_vfat 0 + have_bin mkfs.ntfs && json_add_boolean has_mkfs_ntfs 1 || json_add_boolean has_mkfs_ntfs 0 + have_bin mkfs.exfat && json_add_boolean has_mkfs_exfat 1 || json_add_boolean has_mkfs_exfat 0 + have_bin e2fsck && json_add_boolean has_e2fsck 1 || json_add_boolean has_e2fsck 0 + have_bin fsck.ext4 && json_add_boolean has_fsck_ext4 1 || json_add_boolean has_fsck_ext4 0 + ( have_bin fsck.fat || have_bin fsck.vfat ) && json_add_boolean has_fsck_fat 1 || json_add_boolean has_fsck_fat 0 + have_bin fsck.exfat && json_add_boolean has_fsck_exfat 1 || json_add_boolean has_fsck_exfat 0 + have_bin ntfsfix && json_add_boolean has_ntfsfix 1 || json_add_boolean has_ntfsfix 0 + have_bin fsck && json_add_boolean has_fsck 1 || json_add_boolean has_fsck 0 + have_bin blkid && json_add_boolean has_blkid 1 || json_add_boolean has_blkid 0 + have_fs ext4 kmod-fs-ext4 && json_add_boolean has_kmod_ext4 1 || json_add_boolean has_kmod_ext4 0 + have_fs vfat kmod-fs-vfat && json_add_boolean has_kmod_vfat 1 || json_add_boolean has_kmod_vfat 0 + if have_fs ntfs3 kmod-fs-ntfs3 || grep -qw ntfs /proc/filesystems 2>/dev/null; then + json_add_boolean has_kmod_ntfs 1 + else + json_add_boolean has_kmod_ntfs 0 + fi + have_fs exfat kmod-fs-exfat && json_add_boolean has_kmod_exfat 1 || json_add_boolean has_kmod_exfat 0 + have_fs fuse kmod-fuse && json_add_boolean has_kmod_fuse 1 || json_add_boolean has_kmod_fuse 0 + if [ -d /sys/module/loop ] || have_fs loop kmod-loop; then + json_add_boolean has_kmod_loop 1 + else + json_add_boolean has_kmod_loop 0 + fi + ab=$(apk_bin) + if [ -n "$ab" ]; then + json_add_boolean has_apk 1 + json_add_string apk "$ab" + else + json_add_boolean has_apk 0 + fi + json_dump +} + +json_fail_pkgs() { + json_init + json_add_boolean ok 0 + json_add_string error "$1" + json_add_string need_packages "$2" + [ -n "$3" ] && json_add_string fs_type "$3" + json_dump +} + +fsck_bin_for() { + case $1 in + ext2|ext3|ext4) + have_bin e2fsck && { echo e2fsck; return 0; } + have_bin fsck.ext4 && { echo fsck.ext4; return 0; } + have_bin fsck.ext3 && { echo fsck.ext3; return 0; } + have_bin fsck.ext2 && { echo fsck.ext2; return 0; } + ;; + vfat|fat|fat32|msdos) + have_bin fsck.fat && { echo fsck.fat; return 0; } + have_bin fsck.vfat && { echo fsck.vfat; return 0; } + ;; + exfat) + have_bin fsck.exfat && { echo fsck.exfat; return 0; } + ;; + ntfs) + have_bin ntfsfix && { echo ntfsfix; return 0; } + ;; + ""|none) + have_bin e2fsck && { echo e2fsck; return 0; } + have_bin fsck.fat && { echo fsck.fat; return 0; } + have_bin fsck.exfat && { echo fsck.exfat; return 0; } + have_bin ntfsfix && { echo ntfsfix; return 0; } + have_bin fsck && { echo fsck; return 0; } + ;; + *) + have_bin fsck && { echo fsck; return 0; } + ;; + esac + echo "" +} + +fsck_pkgs_for() { + case $1 in + ext2|ext3|ext4|""|none) echo e2fsprogs ;; + vfat|fat|fat32|msdos) echo dosfstools ;; + exfat) echo exfatprogs ;; + ntfs) echo ntfs-3g ;; + *) echo e2fsprogs ;; + esac +} + +detect_fstype() { + dev=$1 + t= + if have_bin blkid; then + t=$(blkid -s TYPE -o value "$dev" 2>/dev/null | head -n1) + [ -n "$t" ] || t=$(blkid "$dev" 2>/dev/null | sed -n 's/.*TYPE="\([^"]*\)".*/\1/p' | head -n1) + fi + echo "$t" +} + +vc_list_field() { + want_vol=$1 + want_slot=$2 + which=$3 + printf '%s\n' "$4" | while IFS= read -r line; do + [ -n "$line" ] || continue + s=${line%%:*} + rest=${line#*: } + set -- $rest + p=$1 + vd=$2 + mp=$3 + hit=0 + [ -n "$want_slot" ] && [ "$s" = "$want_slot" ] && hit=1 + [ -n "$want_vol" ] && [ "$p" = "$want_vol" ] && hit=1 + [ "$hit" = 1 ] || continue + if [ "$which" = mp ]; then + echo "$mp" + else + echo "$vd" + fi + break + done +} + +vc_unmount_vol() { + vol=$1 + sl=$2 + if [ -n "$sl" ]; then + "$VC" --text --non-interactive --force --unmount --slot="$sl" >/dev/null 2>&1 || true + fi + if [ -n "$vol" ]; then + "$VC" --text --non-interactive --force --unmount "$vol" >/dev/null 2>&1 || true + fi +} + +run_unbuf() { + if command -v stdbuf >/dev/null 2>&1; then + stdbuf -oL -eL "$@" + else + "$@" + fi +} + +run_fsck_cmd() { + tool=$1 + dev=$2 + auto=$3 + case $tool in + e2fsck|fsck.ext2|fsck.ext3|fsck.ext4) + if [ "$auto" = 1 ]; then + run_unbuf "$tool" -f -y -C 0 "$dev" + elif command -v script >/dev/null 2>&1; then + scmd=$(printf '%s -f %s' "$tool" "$dev") + script -c "$scmd" /dev/null + else + run_unbuf "$tool" -f "$dev" + fi + ;; + fsck.fat|fsck.vfat) + if [ "$auto" = 1 ]; then + run_unbuf "$tool" -a "$dev" + else + run_unbuf "$tool" -r "$dev" + fi + ;; + fsck.exfat) + if [ "$auto" = 1 ]; then + run_unbuf "$tool" -y "$dev" 2>/dev/null || run_unbuf "$tool" -p "$dev" + else + run_unbuf "$tool" "$dev" + fi + ;; + ntfsfix) + run_unbuf ntfsfix "$dev" + ;; + fsck) + if [ "$auto" = 1 ]; then + run_unbuf fsck -f -y "$dev" + else + run_unbuf fsck -f "$dev" + fi + ;; + *) + return 127 + ;; + esac +} + +pkg_allowed() { + case $1 in + lvm2|libdevmapper|kmod-dm|e2fsprogs|kmod-fs-ext4|kmod-fs-ext3|dosfstools|kmod-fs-vfat|ntfs-3g|kmod-fs-ntfs3|exfatprogs|kmod-fs-exfat) + return 0 ;; + *) return 1 ;; + esac +} + +cmd_pkg_install() { + read_json + json_get_var packages packages + ab=$(apk_bin) + if [ -z "$ab" ]; then + json_fail "apk not found" + return 0 + fi + safe= + for p in $packages; do + pkg_allowed "$p" || { + json_fail "package not allowed: $p" + return 0 + } + safe="$safe $p" + done + [ -n "$safe" ] || { + json_fail "no packages" + return 0 + } + rm -f "$JOB.rc" "$JOB.log" "$JOB.pid" + # shellcheck disable=SC2086 + ( "$ab" add $safe > "$JOB.log" 2>&1; echo $? > "$JOB.rc" ) & + job_track + json_init + json_add_boolean ok 1 + json_add_boolean pending 1 + json_add_string output "apk add$safe" + json_dump +} + +section_name() { + idx=$1 + line=$(uci show "veracrypt.@volume[$idx]" 2>/dev/null | head -n1) || return 1 + name=${line#veracrypt.} + name=${name%=volume} + [ -n "$name" ] || return 1 + echo "$name" +} + +is_mounted_mp() { + mp=$1 + [ -n "$mp" ] || return 1 + awk -v mp="$mp" '$2 == mp { found=1 } END { exit found ? 0 : 1 }' /proc/mounts +} + +cmd_status() { + json_init + json_add_string version "$("$VC" --text --version 2>/dev/null | head -n1)" + json_add_array volumes + idx=0 + while uci -q get "veracrypt.@volume[$idx]" >/dev/null 2>&1; do + name=$(section_name "$idx") || { idx=$((idx + 1)); continue; } + vol=$(uci -q get "veracrypt.${name}.volume") + mp=$(uci -q get "veracrypt.${name}.mountpoint") + json_add_object + json_add_string name "$name" + json_add_string volume "$vol" + json_add_string mountpoint "$mp" + slot=$(uci -q get "veracrypt.${name}.slot") + json_add_string slot "$slot" + if is_mounted_mp "$mp"; then + json_add_boolean mounted 1 + else + json_add_boolean mounted 0 + fi + json_close_object + idx=$((idx + 1)) + done + json_close_array + + listout=$("$VC" --text --non-interactive --list 2>/dev/null) + json_add_string list "$listout" + + json_add_array slots + used_max=0 + slot=1 + while [ "$slot" -le 64 ]; do + line=$(printf '%s\n' "$listout" | awk -v s="$slot" ' + $1 == s || $1 == s ":" { print; exit } + $1 ~ ("^" s ":") { print; exit } + ') + if [ -n "$line" ]; then + json_add_object + json_add_int slot "$slot" + json_add_boolean used 1 + json_add_string line "$line" + json_close_object + used_max=$slot + fi + slot=$((slot + 1)) + done + json_close_array + next=$((used_max + 1)) + [ "$next" -gt 64 ] && next=0 + json_add_int next_slot "$next" + json_dump +} + +cmd_listdev() { + json_init + json_add_boolean ok 1 + json_add_array devices + # /sys/class/block is reliable on OpenWrt; /dev/sd* globs often miss USB names. + if [ -d /sys/class/block ]; then + for d in /sys/class/block/*; do + [ -e "$d" ] || continue + name=${d##*/} + case $name in + ram*|zram*) continue ;; + esac + path=/dev/$name + [ -e "$path" ] || continue + json_add_object + json_add_string path "$path" + json_add_string name "$name" + json_close_object + done + fi + if [ -d /dev/mapper ]; then + for p in /dev/mapper/*; do + case $p in *'*'*) continue ;; esac + [ -e "$p" ] || continue + json_add_object + json_add_string path "$p" + json_add_string name "mapper/${p##*/}" + json_close_object + done + fi + json_close_array + json_dump +} + +job_track() { + echo $! > "$JOB.pid" + if [ -n "$1" ]; then + printf '%s\n' "$1" > "$JOB.vol" + else + rm -f "$JOB.vol" + fi + if [ -n "$2" ]; then + printf '%s\n' "$2" > "$JOB.slot" + else + rm -f "$JOB.slot" + fi +} + +# VeraCrypt progress uses CR; collapse to unique last progress line. +job_log_cooked() { + [ -f "$JOB.log" ] || return 0 + tr '\r' '\n' < "$JOB.log" 2>/dev/null | awk ' + /Done:|Speed:|Left:/ { + prog=$0 + next + } + NF { print } + END { if (prog != "") print prog } + ' +} + +summarize_log() { + job_log_cooked | grep -v '^[[:space:]]*$' | tail -n 20 +} + +job_log_text() { + n=${1:-80} + job_log_cooked | tail -n "$n" +} + +cmd_job() { + if [ -f "$JOB.rc" ]; then + rc=$(cat "$JOB.rc" 2>/dev/null) + out=$(summarize_log) + log=$(job_log_text 400) + if [ -f "$JOB.need" ]; then + pkgs=$(cat "$JOB.need" 2>/dev/null) + fst=$(cat "$JOB.fstype" 2>/dev/null) + json_init + json_add_boolean ok 0 + json_add_string error "$out" + json_add_string output "$log" + json_add_string need_packages "$pkgs" + [ -n "$fst" ] && json_add_string fs_type "$fst" + json_dump + return 0 + fi + if grep -q 'dmsetup not found' "$JOB.log" 2>/dev/null && grep -q '100.000%' "$JOB.log" 2>/dev/null; then + json_init + json_add_boolean ok 1 + json_add_string output "$log" + json_add_string summary "Container file was created. Inner filesystem was not formatted because dmsetup is not installed (OpenWrt package lvm2). Create with filesystem=none, then mount and run mkfs, or apk add lvm2. $out" + json_dump + return 0 + fi + json_init + if [ "$rc" = 0 ]; then + json_add_boolean ok 1 + else + json_add_boolean ok 0 + json_add_string error "$out" + fi + json_add_string output "$log" + json_dump + return 0 + fi + if [ -f "$JOB.pid" ]; then + pid=$(cat "$JOB.pid" 2>/dev/null) + if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then + json_init + json_add_boolean ok 1 + json_add_boolean pending 1 + [ -f "$JOB.ask" ] && json_add_boolean interactive 1 + json_add_string output "$(job_log_text 250)" + json_dump + return 0 + fi + fi + json_fail "no job" +} + +cmd_job_log() { + json_init + json_add_boolean ok 1 + if [ -f "$JOB.log" ]; then + json_add_string output "$(tail -c 200000 "$JOB.log" 2>/dev/null)" + else + json_add_string output "" + fi + json_dump +} + +kill_tree() { + _p=$1 + _sig=${2:-TERM} + [ -n "$_p" ] && [ -d "/proc/$_p" ] || return 0 + kids= + if [ -r "/proc/$_p/task/$_p/children" ]; then + kids=$(cat "/proc/$_p/task/$_p/children" 2>/dev/null) + fi + if [ -z "$kids" ]; then + for d in /proc/[0-9]*; do + pp=$(sed -n 's/^PPid:[[:space:]]*//p' "$d/status" 2>/dev/null) + [ "$pp" = "$_p" ] && kids="$kids ${d#/proc/}" + done + fi + for c in $kids; do + kill_tree "$c" "$_sig" + done + kill -s "$_sig" "$_p" 2>/dev/null || true +} + +cmd_job_abort() { + pid=$(cat "$JOB.pid" 2>/dev/null) + vol=$(cat "$JOB.vol" 2>/dev/null) + sl=$(cat "$JOB.slot" 2>/dev/null) + if [ -n "$pid" ] && [ -d "/proc/$pid" ]; then + printf '\nabort requested\n' >> "$JOB.log" 2>/dev/null || true + kill_tree "$pid" TERM + n=0 + while [ -d "/proc/$pid" ] && [ "$n" -lt 15 ]; do + n=$((n + 1)) + sleep 1 + done + kill_tree "$pid" KILL + sleep 1 + fi + vc_unmount_vol "$vol" "$sl" + printf 'aborted (unmounted)\n' >> "$JOB.log" 2>/dev/null || true + echo 143 > "$JOB.rc" + rm -f "$JOB.pid" "$JOB.ask" "$JOB.in" "$JOB.vol" "$JOB.slot" + json_init + json_add_boolean ok 1 + json_add_string output "Aborted. The router job was stopped and the volume was unmounted." + json_dump +} + +cmd_job_dismount() { + vol=$(cat "$JOB.vol" 2>/dev/null) + sl=$(cat "$JOB.slot" 2>/dev/null) + if [ -z "$vol" ] && [ -z "$sl" ]; then + json_ok "nothing to unmount" + return 0 + fi + printf '\nunmount after Close\n' >> "$JOB.log" 2>/dev/null || true + vc_unmount_vol "$vol" "$sl" + rm -f "$JOB.vol" "$JOB.slot" + json_ok "unmounted" +} + +cmd_job_answer() { + read_json + json_get_var answer answer + case $answer in + y|Y|yes) a=y ;; + n|N|no) a=n ;; + *) + json_fail "answer must be y or n" + return 0 + ;; + esac + if [ ! -p "$JOB.in" ]; then + json_fail "no interactive fsck" + return 0 + fi + if command -v timeout >/dev/null 2>&1; then + timeout 3 sh -c "printf '%s\n' '$a' > '$JOB.in'" 2>/dev/null || true + else + printf '%s\n' "$a" > "$JOB.in" & + fi + json_ok +} + +cmd_listdir() { + read_json + json_get_var path path + [ -n "$path" ] || path=/mnt + if ! valid_path "$path"; then + json_fail "invalid path" + return 0 + fi + if [ ! -d "$path" ]; then + json_fail "not a directory" + return 0 + fi + tmp=$(vc_tmp) + : > "$tmp" + # -p marks dirs with / so we do not stat every name (can hang on FUSE). + if command -v timeout >/dev/null 2>&1; then + timeout 5 ls -1Ap "$path" > "$tmp" 2>/dev/null || true + else + ls -1Ap "$path" > "$tmp" 2>/dev/null || true + fi + json_init + json_add_boolean ok 1 + json_add_string path "$path" + json_add_array entries + parent=$(dirname "$path") + json_add_object + json_add_string name .. + json_add_string type dir + json_add_string path "$parent" + json_close_object + n=0 + while IFS= read -r ent; do + [ -n "$ent" ] || continue + [ "$ent" = ./ ] && continue + [ "$ent" = ../ ] && continue + n=$((n + 1)) + [ "$n" -gt 400 ] && break + t=file + case $ent in + */) t=dir; ent=${ent%/} ;; + esac + fp="$path/$ent" + json_add_object + json_add_string name "$ent" + json_add_string type "$t" + json_add_string path "$fp" + json_close_object + done < "$tmp" + rm -f "$tmp" + json_close_array + json_dump +} + +cmd_mkdir() { + read_json + json_get_var path path + if [ -z "$path" ] || ! valid_path "$path"; then + json_fail "invalid path" + return 0 + fi + case $path in + /|/mnt|/mnt/) + json_fail "refusing to create $path; use a subdirectory such as /mnt/Buffalo" + return 0 + ;; + esac + err=$(vc_tmp) + if ! mkdir -p "$path" 2>"$err"; then + json_fail "$(cat "$err" 2>/dev/null)" + rm -f "$err" + return 0 + fi + rm -f "$err" + if [ ! -d "$path" ]; then + json_fail "not a directory after mkdir" + return 0 + fi + json_ok "$path" +} + +protected_rm_path() { + p=$1 + case $p in + /|/mnt|/mnt/|/overlay|/overlay/|/rom|/rom/|/proc|/sys|/dev|/tmp|/etc|/usr|/bin|/sbin|/lib|/www|/root|/boot) + return 0 ;; + esac + case $p in + /dev/*|/proc/*|/sys/*|/etc/*|/usr/*|/bin/*|/sbin/*|/lib/*|/rom/*|/overlay/*|/www/*|/root/*|/boot/*|/tmp/luci-veracrypt-job*) + return 0 ;; + esac + if [ -b "$p" ] || [ -c "$p" ]; then + return 0 + fi + if awk -v mp="$p" '$2 == mp { found=1 } END { exit found ? 0 : 1 }' /proc/mounts; then + return 0 + fi + return 1 +} + +cmd_rm() { + read_json + json_get_var path path + json_get_var recursive recursive + if [ -z "$path" ] || ! valid_path "$path"; then + json_fail "invalid path" + return 0 + fi + if protected_rm_path "$path"; then + json_fail "refusing to delete $path" + return 0 + fi + if [ ! -e "$path" ] && [ ! -L "$path" ]; then + json_fail "not found" + return 0 + fi + err=$(vc_tmp) + if [ -d "$path" ] && [ ! -L "$path" ]; then + if [ "$recursive" = 1 ]; then + if ! rm -r "$path" 2>"$err"; then + json_fail "$(cat "$err" 2>/dev/null)" + rm -f "$err" + return 0 + fi + else + if ! rmdir "$path" 2>"$err"; then + rm -f "$err" + json_init + json_add_boolean ok 0 + json_add_string error "directory is not empty" + json_add_boolean need_recursive 1 + json_dump + return 0 + fi + fi + else + if ! rm -f "$path" 2>"$err"; then + json_fail "$(cat "$err" 2>/dev/null)" + rm -f "$err" + return 0 + fi + fi + rm -f "$err" + json_ok "$path" +} + +cmd_run() { + read_json + json_get_var action action + json_get_var name name + json_get_var volume volume + json_get_var mountpoint mountpoint + json_get_var password password + json_get_var new_password new_password + json_get_var pim pim + json_get_var new_pim new_pim + json_get_var hash hash + json_get_var new_hash new_hash + json_get_var encryption encryption + json_get_var filesystem filesystem + json_get_var fs_options fs_options + json_get_var keyfiles keyfiles + json_get_var new_keyfiles new_keyfiles + json_get_var protect_hidden protect_hidden + json_get_var protection_password protection_password + json_get_var protection_pim protection_pim + json_get_var protection_hash protection_hash + json_get_var protection_keyfiles protection_keyfiles + json_get_var slot slot + json_get_var size size + json_get_var volume_type volume_type + json_get_var random_source random_source + json_get_var token_lib token_lib + json_get_var token_pin token_pin + json_get_var mount_options mount_options + json_get_var auto_mount auto_mount + json_get_var force force + json_get_var quick quick + json_get_var no_size_check no_size_check + json_get_var legacy_password_maxlength legacy_password_maxlength + json_get_var allow_insecure_mount allow_insecure_mount + json_get_var fsck_auto fsck_auto + json_get_var backup_file backup_file + use_tc= + + if [ -n "$name" ]; then + if ! valid_name "$name"; then + json_fail "invalid volume name" + return 0 + fi + [ -n "$volume" ] || volume=$(uci -q get "veracrypt.${name}.volume") + [ -n "$mountpoint" ] || mountpoint=$(uci -q get "veracrypt.${name}.mountpoint") + [ -n "$pim" ] || pim=$(uci -q get "veracrypt.${name}.pim") + [ -n "$protect_hidden" ] || protect_hidden=$(uci -q get "veracrypt.${name}.protect_hidden") + [ -n "$keyfiles" ] || keyfiles=$(uci -q get "veracrypt.${name}.keyfiles") + [ -n "$filesystem" ] || filesystem=$(uci -q get "veracrypt.${name}.filesystem") + [ -n "$slot" ] || slot=$(uci -q get "veracrypt.${name}.slot") + [ -n "$hash" ] || hash=$(uci -q get "veracrypt.${name}.hash") + [ -n "$encryption" ] || encryption=$(uci -q get "veracrypt.${name}.encryption") + [ -n "$mount_options" ] || mount_options=$(uci -q get "veracrypt.${name}.mount_options") + nokc=$(uci -q get "veracrypt.${name}.nokernelcrypto") + tc=$(uci -q get "veracrypt.${name}.truecrypt") + if [ "$nokc" = 1 ]; then + case $mount_options in + *nokernelcrypto*) ;; + "") mount_options=nokernelcrypto ;; + *) mount_options="$mount_options,nokernelcrypto" ;; + esac + fi + [ "$tc" = 1 ] && use_tc=1 + fi + + if ! valid_action "$action"; then + json_fail "invalid action" + return 0 + fi + if ! valid_path "$volume" || ! valid_path "$mountpoint" || ! valid_path "$token_lib" \ + || ! valid_path "$backup_file"; then + json_fail "paths must be absolute and must not contain shell metacharacters" + return 0 + fi + if ! valid_keyfiles "$keyfiles" || ! valid_keyfiles "$new_keyfiles" \ + || ! valid_keyfiles "$protection_keyfiles"; then + json_fail "keyfiles must be absolute files or directories, or token:// / emv:// paths" + return 0 + fi + if ! valid_random "$random_source"; then + json_fail "random source must be /dev/urandom or /dev/random" + return 0 + fi + if ! valid_slot "$slot" || ! valid_hash "$hash" || ! valid_hash "$new_hash" \ + || ! valid_hash "$protection_hash" || ! valid_encryption "$encryption" \ + || ! valid_filesystem "$filesystem" || ! valid_volume_type "$volume_type" \ + || ! valid_protect_hidden "$protect_hidden" || ! valid_auto_mount "$auto_mount" \ + || ! valid_pim "$pim" || ! valid_pim "$new_pim" || ! valid_pim "$protection_pim" \ + || ! valid_size "$size" || ! valid_flag "$force" || ! valid_flag "$quick" \ + || ! valid_flag "$no_size_check" \ + || ! valid_flag "$legacy_password_maxlength" || ! valid_flag "$allow_insecure_mount" \ + || ! valid_mount_options "$mount_options" || ! valid_fs_options "$fs_options"; then + json_fail "invalid option value" + return 0 + fi + + case $action in + create) + [ -n "$encryption" ] || encryption=AES-Twofish-Serpent + [ -n "$hash" ] || hash=sha-512 + [ -n "$volume_type" ] || volume_type=normal + [ -n "$filesystem" ] || filesystem=none + [ -n "$random_source" ] || random_source=/dev/urandom + new_password= + new_pim= + new_hash= + new_keyfiles= + mount_options= + auto_mount= + fs_options= + protect_hidden= + protection_password= + protection_pim= + protection_hash= + protection_keyfiles= + slot= + allow_insecure_mount= + no_size_check= + legacy_password_maxlength= + ;; + mount|auto-mount) + encryption= + hash= + size= + volume_type= + filesystem= + quick= + new_password= + new_pim= + new_hash= + new_keyfiles= + random_source= + fs_options= + no_size_check= + [ -n "$mount_options" ] || mount_options=nokernelcrypto + [ -n "$pim" ] || pim=0 + [ -n "$protect_hidden" ] || protect_hidden=no + case $mount_options in + truecrypt) mount_options=nokernelcrypto ;; + truecrypt,*) mount_options=${mount_options#truecrypt,} ;; + *,truecrypt) mount_options=${mount_options%,truecrypt} ;; + *,truecrypt,*) mount_options=$(echo "$mount_options" | sed 's/,truecrypt,/,/g;s/^truecrypt,//;s/,truecrypt$//') ;; + esac + if [ "$action" = auto-mount ]; then + [ -n "$auto_mount" ] || auto_mount=favorites + else + auto_mount= + fi + ;; + change) + encryption= + filesystem= + size= + volume_type= + quick= + mount_options= + auto_mount= + slot= + mountpoint= + fs_options= + protect_hidden= + random_source= + allow_insecure_mount= + no_size_check= + ;; + backup-headers|restore-headers) + encryption= + filesystem= + size= + volume_type= + quick= + mount_options= + auto_mount= + slot= + mountpoint= + new_password= + new_pim= + new_hash= + new_keyfiles= + fs_options= + protect_hidden= + allow_insecure_mount= + no_size_check= + ;; + create-keyfile) + password= + encryption= + hash= + filesystem= + size= + volume_type= + quick= + mount_options= + auto_mount= + slot= + mountpoint= + pim= + keyfiles= + new_password= + fs_options= + protect_hidden= + [ -n "$random_source" ] || random_source=/dev/urandom + ;; + volume-properties) + password= + encryption= + hash= + filesystem= + size= + quick= + mount_options= + pim= + keyfiles= + ;; + esac + + set -- --text --non-interactive + [ "$use_tc" = 1 ] && set -- "$@" --truecrypt + [ "$force" = 1 ] && set -- "$@" --force + [ "$quick" = 1 ] && set -- "$@" --quick + [ "$no_size_check" = 1 ] && set -- "$@" --no-size-check + [ "$legacy_password_maxlength" = 1 ] && set -- "$@" --legacy-password-maxlength + [ "$allow_insecure_mount" = 1 ] && set -- "$@" --allow-insecure-mount + [ -n "$pim" ] && set -- "$@" --pim="$pim" + [ -n "$new_pim" ] && set -- "$@" --new-pim="$new_pim" + [ -n "$hash" ] && set -- "$@" --hash="$hash" + [ -n "$new_hash" ] && set -- "$@" --new-hash="$new_hash" + [ -n "$encryption" ] && set -- "$@" --encryption="$encryption" + [ -n "$filesystem" ] && set -- "$@" --filesystem="$filesystem" + [ -n "$fs_options" ] && set -- "$@" --fs-options="$fs_options" + [ -n "$keyfiles" ] && set -- "$@" --keyfiles="$keyfiles" + [ -n "$new_keyfiles" ] && set -- "$@" --new-keyfiles="$new_keyfiles" + [ -n "$protect_hidden" ] && set -- "$@" --protect-hidden="$protect_hidden" + [ -n "$protection_pim" ] && set -- "$@" --protection-pim="$protection_pim" + [ -n "$protection_hash" ] && set -- "$@" --protection-hash="$protection_hash" + [ -n "$protection_keyfiles" ] && set -- "$@" --protection-keyfiles="$protection_keyfiles" + [ -n "$slot" ] && set -- "$@" --slot="$slot" + [ -n "$size" ] && set -- "$@" --size="$size" + [ -n "$volume_type" ] && set -- "$@" --volume-type="$volume_type" + [ -n "$random_source" ] && set -- "$@" --random-source="$random_source" + [ -n "$token_lib" ] && set -- "$@" --token-lib="$token_lib" + [ -n "$mount_options" ] && set -- "$@" --mount-options="$mount_options" + [ -n "$auto_mount" ] && set -- "$@" --auto-mount="$auto_mount" + + out= + code=0 + case $action in + mount) + if ! valid_path "$volume" || [ -z "$volume" ] || ! valid_path "$mountpoint" || [ -z "$mountpoint" ]; then + json_fail "volume and mountpoint required" + return 0 + fi + case $mountpoint in + /|/mnt|/mnt/) + json_fail "refusing to mount on $mountpoint; use a subdirectory such as /mnt/Buffalo" + return 0 + ;; + esac + case $volume in + "$mountpoint"/*) + json_fail "container $volume is inside $mountpoint; mounting there would hide the file (busy unmount). Use e.g. /mnt/Buffalo" + return 0 + ;; + esac + mkdir -p "$mountpoint" 2>/dev/null || true + rm -f "$JOB.rc" "$JOB.log" "$JOB.pid" + ( vc_run "$@" "$volume" "$mountpoint" > "$JOB.log" 2>&1; echo $? > "$JOB.rc" ) & + job_track "$volume" + json_init + json_add_boolean ok 1 + json_add_boolean pending 1 + json_add_string output "mount started" + json_dump + return 0 + ;; + unmount|dismount) + rm -f "$JOB.rc" "$JOB.log" "$JOB.pid" + ( + # VeraCrypt first. umount -l beforehand yields "No such volume is mounted". + # Never fall through to unmount-all unless slot, volume and mountpoint are all empty. + if [ -n "$slot" ]; then + "$VC" --text --non-interactive --force --unmount --slot="$slot" || true + fi + if [ -n "$volume" ]; then + "$VC" --text --non-interactive --force --unmount "$volume" || true + fi + if [ -n "$mountpoint" ] && [ "$mountpoint" != "$volume" ]; then + "$VC" --text --non-interactive --force --unmount "$mountpoint" || true + fi + if [ -z "$slot" ] && [ -z "$volume" ] && [ -z "$mountpoint" ]; then + "$VC" --text --non-interactive --force --unmount || true + fi + if [ -n "$mountpoint" ]; then + umount -l "$mountpoint" || true + fi + listout=$("$VC" --text --non-interactive --list 2>/dev/null) + still=0 + if [ -n "$slot" ]; then + printf '%s\n' "$listout" | awk -v s="$slot" '$1 == s || $1 == s ":" { found=1 } END { exit found ? 0 : 1 }' && still=1 + fi + if [ -n "$volume" ]; then + printf '%s\n' "$listout" | grep -F -q "$volume" && still=1 + fi + if [ -n "$mountpoint" ] && is_mounted_mp "$mountpoint"; then + still=1 + fi + if [ -z "$slot" ] && [ -z "$volume" ] && [ -z "$mountpoint" ]; then + [ -n "$listout" ] && still=1 + fi + if [ "$still" = 0 ]; then + echo 0 > "$JOB.rc" + else + echo 1 > "$JOB.rc" + fi + ) > "$JOB.log" 2>&1 & + job_track "$volume" "$slot" + json_init + json_add_boolean ok 1 + json_add_boolean pending 1 + json_add_string output "unmount started" + json_dump + return 0 + ;; + fsck) + if [ -z "$volume" ]; then + json_fail "volume path required" + return 0 + fi + if [ -z "$password" ]; then + json_fail "password required" + return 0 + fi + [ -n "$pim" ] || pim=0 + [ -n "$protect_hidden" ] || protect_hidden=no + [ -n "$mount_options" ] || mount_options=nokernelcrypto + case $fsck_auto in + 0|no|false|off) fsck_auto=0 ;; + *) fsck_auto=1 ;; + esac + hint=$filesystem + [ "$hint" = none ] && hint= + if [ -n "$hint" ]; then + tool=$(fsck_bin_for "$hint") + if [ -z "$tool" ]; then + pkgs=$(fsck_pkgs_for "$hint") + json_fail_pkgs "fsck tools for $hint are not installed. Checking a volume cannot be done without them. apk add $pkgs" "$pkgs" "$hint" + return 0 + fi + else + tool=$(fsck_bin_for "") + if [ -z "$tool" ]; then + json_fail_pkgs "No fsck tools installed (e2fsck, fsck.fat, fsck.exfat, ntfsfix). Checking a volume cannot be done without them. apk add e2fsprogs (ext*), dosfstools (FAT), exfatprogs (exFAT), or ntfs-3g (NTFS)." "e2fsprogs" "" + return 0 + fi + fi + listout=$("$VC" --text --non-interactive --list 2>/dev/null) + curmp=$(vc_list_field "$volume" "$slot" mp "$listout") + if [ -n "$curmp" ] && [ "$curmp" != "-" ] && is_mounted_mp "$curmp"; then + json_fail "volume is mounted on $curmp; unmount first. fsck decrypts with --filesystem=none then runs fsck on the mapper/loop device" + return 0 + fi + vol_path=$volume + pw=$password + kf=$keyfiles + pim_m=$pim + ph=$protect_hidden + mopt=$mount_options + sl=$slot + auto=$fsck_auto + hint_m=$hint + tcflag= + [ "$use_tc" = 1 ] && tcflag=--truecrypt + rm -f "$JOB.rc" "$JOB.log" "$JOB.pid" "$JOB.need" "$JOB.fstype" "$JOB.ask" "$JOB.in" + if [ "$auto" != 1 ]; then + mkfifo "$JOB.in" + : > "$JOB.ask" + fi + ( + if [ "$auto" != 1 ]; then + exec 3<>"$JOB.in" + fi + { + listout=$("$VC" --text --non-interactive --list 2>/dev/null) + vdev=$(vc_list_field "$vol_path" "$sl" vdev "$listout") + [ "$vdev" = "-" ] && vdev= + if [ -n "$vdev" ] && [ -e "$vdev" ]; then + printf 'already mapped %s, skipping decrypt\n' "$vdev" + mrc=0 + else + printf 'Decrypt %s --filesystem=none\n' "$vol_path" + set -- --text --non-interactive --filesystem=none \ + --pim="$pim_m" --protect-hidden="$ph" \ + --mount-options="$mopt" + [ -n "$tcflag" ] && set -- "$@" --truecrypt + [ -n "$kf" ] && set -- "$@" --keyfiles="$kf" + [ -n "$sl" ] && set -- "$@" --slot="$sl" + vc_run "$@" "$vol_path" + mrc=$? + fi + if [ "$mrc" -ne 0 ]; then + echo "decrypt failed ($mrc)" + echo "$mrc" > "$JOB.rc" + exit 0 + fi + listout=$("$VC" --text --non-interactive --list 2>/dev/null) + printf 'veracrypt -l\n%s\n' "$listout" + vdev=$(vc_list_field "$vol_path" "$sl" vdev "$listout") + [ "$vdev" = "-" ] && vdev= + n=0 + while [ -z "$vdev" ] || [ ! -e "$vdev" ]; do + n=$((n + 1)) + [ "$n" -gt 20 ] && break + sleep 1 + listout=$("$VC" --text --non-interactive --list 2>/dev/null) + vdev=$(vc_list_field "$vol_path" "$sl" vdev "$listout") + [ "$vdev" = "-" ] && vdev= + done + if [ -z "$vdev" ] || [ ! -e "$vdev" ] || ! valid_vdev "$vdev"; then + echo "no mapper/loop device in veracrypt -l (expected /dev/mapper/veracryptN or /dev/loopN)" + vc_unmount_vol "$vol_path" "$sl" + echo 1 > "$JOB.rc" + exit 0 + fi + printf 'raw volume: %s\n' "$vdev" + fst=$(detect_fstype "$vdev") + [ -n "$fst" ] || fst=$hint_m + printf 'filesystem: %s\n' "${fst:-unknown}" + tool=$(fsck_bin_for "$fst") + if [ -z "$tool" ]; then + pkgs=$(fsck_pkgs_for "$fst") + printf 'fsck tools for %s are not installed. Checking a volume cannot be done without them. apk add %s\n' "${fst:-unknown}" "$pkgs" + echo "$pkgs" > "$JOB.need" + echo "$fst" > "$JOB.fstype" + vc_unmount_vol "$vol_path" "$sl" + echo 1 > "$JOB.rc" + exit 0 + fi + if [ "$auto" = 1 ]; then + printf 'fsck automatic yes: %s -f -y %s\n' "$tool" "$vdev" + run_fsck_cmd "$tool" "$vdev" 1 + frc=$? + else + printf 'fsck interactive: %s %s (answer y or n)\n' "$tool" "$vdev" + run_fsck_cmd "$tool" "$vdev" 0 < "$JOB.in" + frc=$? + fi + printf 'fsck exit %s\n' "$frc" + printf 'fsck finished. Volume is still decrypted (--filesystem=none), not mounted as a filesystem.\nClose this dialog to unmount.\n' + "$VC" --text --non-interactive --list 2>/dev/null || true + case $frc in + 0|1|2) echo 0 > "$JOB.rc" ;; + *) echo "$frc" > "$JOB.rc" ;; + esac + } > "$JOB.log" 2>&1 + ) & + job_track "$vol_path" "$sl" + json_init + json_add_boolean ok 1 + json_add_boolean pending 1 + [ "$auto" != 1 ] && json_add_boolean interactive 1 + json_add_string output "fsck started (--filesystem=none, then fsck; Close to unmount)" + json_dump + return 0 + ;; + create) + missing= + [ -n "$volume" ] || missing="$missing volume path (folder + file name);" + [ -n "$size" ] || missing="$missing --size (container size, e.g. 100M);" + [ -n "$password" ] || missing="$missing password;" + if [ -n "$missing" ]; then + json_fail "required argument missing:$missing" + return 0 + fi + [ -n "$encryption" ] || encryption=AES-Twofish-Serpent + [ -n "$hash" ] || hash=sha-512 + [ -n "$volume_type" ] || volume_type=normal + [ -n "$filesystem" ] || filesystem=none + [ -n "$random_source" ] || random_source=/dev/urandom + if [ "$filesystem" != none ] && ! have_bin mkfs.ext4 && [ "$filesystem" = ext4 -o "$filesystem" = ext3 -o "$filesystem" = ext2 ]; then + json_fail "filesystem $filesystem needs mkfs.ext4 (apk add e2fsprogs). Use filesystem=none, or install e2fsprogs first." + return 0 + fi + listout=$("$VC" --text --non-interactive --list 2>/dev/null) + used_max=0 + s=1 + while [ "$s" -le 64 ]; do + line=$(printf '%s\n' "$listout" | awk -v sl="$s" '$1 == sl || $1 == sl ":" { print; exit }') + [ -n "$line" ] && used_max=$s + s=$((s + 1)) + done + nslot=$((used_max + 1)) + [ "$nslot" -lt 1 ] && nslot=1 + [ "$nslot" -gt 64 ] && nslot=1 + base=${volume##*/} + base=${base%.*} + [ -n "$base" ] || base=veracrypt + mp=$mountpoint + [ -n "$mp" ] || mp=/mnt/$base + case $mp in /|/mnt|/mnt/) mp=/mnt/vc$nslot ;; esac + case $volume in + "$mp"/*) mp=/mnt/vc$nslot ;; + esac + vol_path=$volume + pw=$password + kf=$keyfiles + pim_m=${pim:-0} + qflag= + fflag= + [ "$quick" = 1 ] && qflag=--quick + [ "$force" = 1 ] && fflag=--force + pimflag= + [ -n "$pim" ] && pimflag=--pim + rm -f "$JOB.rc" "$JOB.log" "$JOB.pid" + ( + set -- --text --non-interactive --create \ + --volume-type="$volume_type" \ + --size="$size" \ + --encryption="$encryption" \ + --hash="$hash" \ + --filesystem="$filesystem" \ + --random-source="$random_source" + [ -n "$pimflag" ] && set -- "$@" --pim="$pim" + [ -n "$qflag" ] && set -- "$@" --quick + [ -n "$fflag" ] && set -- "$@" --force + [ -n "$kf" ] && set -- "$@" --keyfiles="$kf" + vc_run "$@" "$vol_path" > "$JOB.log" 2>&1 + crc=$? + created=0 + if [ "$crc" -eq 0 ]; then + created=1 + elif grep -q 'dmsetup not found' "$JOB.log" 2>/dev/null && grep -q '100.000%' "$JOB.log" 2>/dev/null; then + created=1 + fi + if [ "$created" -eq 1 ]; then + mkdir -p "$mp" 2>/dev/null || true + set -- --text --non-interactive \ + --pim="$pim_m" --protect-hidden=no \ + --mount-options=nokernelcrypto \ + --slot="$nslot" + [ -n "$kf" ] && set -- "$@" --keyfiles="$kf" + if [ "$filesystem" = none ]; then + printf 'filesystem=none: mapping without mounting an inner filesystem (%s -> %s slot %s)\n' \ + "$vol_path" "$mp" "$nslot" >> "$JOB.log" + vc_run "$@" --filesystem=none \ + "$vol_path" "$mp" >> "$JOB.log" 2>&1 || true + else + printf 'Automount %s -> %s slot %s\n' "$vol_path" "$mp" "$nslot" >> "$JOB.log" + vc_run "$@" \ + "$vol_path" "$mp" >> "$JOB.log" 2>&1 + am=$? + if [ "$am" -ne 0 ]; then + printf '\nInner filesystem mount failed (container was still created). If the inner FS is missing, format after mapping with filesystem=none, or recreate with an inner filesystem whose mkfs/kmod are installed.\n' >> "$JOB.log" + fi + fi + echo 0 > "$JOB.rc" + else + echo "$crc" > "$JOB.rc" + fi + ) & + job_track "$vol_path" + json_init + json_add_boolean ok 1 + json_add_boolean pending 1 + json_add_string output "create started; will automount to $mp slot $nslot" + json_dump + return 0 + ;; + change) + if [ -z "$volume" ]; then + json_fail "volume path required" + return 0 + fi + if [ -z "$new_password" ]; then + json_fail "new password required" + return 0 + fi + rm -f "$JOB.rc" "$JOB.log" "$JOB.pid" + ( + [ -n "$pim" ] || pim=0 + [ -n "$new_pim" ] || new_pim=0 + set -- --text --random-source=/dev/urandom --change \ + --pim="$pim" --new-pim="$new_pim" \ + --keyfiles="$keyfiles" --new-keyfiles="$new_keyfiles" + [ -n "$hash" ] && set -- "$@" --hash="$hash" + [ -n "$new_hash" ] && set -- "$@" --new-hash="$new_hash" + { + printf '%s\n' "$password" + printf '%s\n' "$new_password" + printf '%s\n' "$new_password" + [ -n "$token_pin" ] && printf '%s\n' "$token_pin" + } | "$VC" "$@" "$volume" > "$JOB.log" 2>&1 + echo $? > "$JOB.rc" + ) & + job_track "$volume" + json_init + json_add_boolean ok 1 + json_add_boolean pending 1 + json_add_string output "change started" + json_dump + return 0 + ;; + list) + out=$("$VC" "$@" --list 2>&1) + code=$? + ;; + test) + rm -f "$JOB.rc" "$JOB.log" "$JOB.pid" + ( "$VC" --text --non-interactive --test > "$JOB.log" 2>&1; echo $? > "$JOB.rc" ) & + job_track + json_init + json_add_boolean ok 1 + json_add_boolean pending 1 + json_add_string output "test started" + json_dump + return 0 + ;; + version) + out=$("$VC" --text --version 2>&1) + code=$? + ;; + help) + out=$("$VC" --text --help 2>&1) + code=$? + ;; + backup-headers) + if [ -z "$volume" ] || ! valid_path "$volume"; then + json_fail "volume path required" + return 0 + fi + if [ -z "$backup_file" ] || ! valid_path "$backup_file"; then + json_fail "header backup file path required (where the backup is written)" + return 0 + fi + if [ -z "$password" ]; then + json_fail "password required" + return 0 + fi + rm -f "$JOB.rc" "$JOB.log" "$JOB.pid" + ( + { + printf '%s\n' "$password" + printf '%s\n' "$pim" + [ -n "$keyfiles" ] && printf '%s\n' "$keyfiles" + printf '\n' + if [ -n "$protection_password" ]; then + printf 'y\n' + printf '%s\n' "$protection_password" + printf '%s\n' "$protection_pim" + printf '\n' + else + printf 'n\n' + fi + printf 'y\n' + printf '%s\n' "$backup_file" + } | "$VC" --text --random-source=/dev/urandom --backup-headers "$volume" > "$JOB.log" 2>&1 + echo $? > "$JOB.rc" + printf '\nHeader backup file: %s\n' "$backup_file" >> "$JOB.log" + ) & + job_track "$volume" + json_init + json_add_boolean ok 1 + json_add_boolean pending 1 + json_add_string output "backup-headers started; file $backup_file" + json_dump + return 0 + ;; + restore-headers) + if [ -z "$volume" ] || ! valid_path "$volume"; then + json_fail "volume path required (container to restore into)" + return 0 + fi + if [ -z "$backup_file" ] || ! valid_path "$backup_file"; then + json_fail "header backup file path required (file to restore from)" + return 0 + fi + if [ -z "$password" ]; then + json_fail "password required" + return 0 + fi + rm -f "$JOB.rc" "$JOB.log" "$JOB.pid" + ( + { + printf '2\n' + printf 'y\n' + printf '%s\n' "$backup_file" + printf '%s\n' "$password" + printf '%s\n' "$pim" + [ -n "$keyfiles" ] && printf '%s\n' "$keyfiles" + printf '\n' + } | "$VC" --text --random-source=/dev/urandom --restore-headers "$volume" > "$JOB.log" 2>&1 + echo $? > "$JOB.rc" + printf '\nRestored from: %s into %s\n' "$backup_file" "$volume" >> "$JOB.log" + ) & + job_track "$volume" + json_init + json_add_boolean ok 1 + json_add_boolean pending 1 + json_add_string output "restore-headers started from $backup_file" + json_dump + return 0 + ;; + create-keyfile) + if [ -z "$volume" ]; then + json_fail "keyfile path required (directory + file name)" + return 0 + fi + if [ -d "$volume" ]; then + json_fail "Is a directory: $volume. Create keyfile writes a new file. Use e.g. $volume/keyfile" + return 0 + fi + parent=${volume%/*} + if [ -z "$parent" ] || [ "$parent" = "$volume" ]; then + json_fail "keyfile path must be an absolute file name" + return 0 + fi + if [ ! -d "$parent" ]; then + json_fail "directory $parent does not exist" + return 0 + fi + [ -n "$random_source" ] || set -- "$@" --random-source=/dev/urandom + rm -f "$JOB.rc" "$JOB.log" "$JOB.pid" + ( "$VC" "$@" --create-keyfile "$volume" > "$JOB.log" 2>&1; echo $? > "$JOB.rc" ) & + job_track + json_init + json_add_boolean ok 1 + json_add_boolean pending 1 + json_add_string output "create-keyfile started" + json_dump + return 0 + ;; + volume-properties) + if [ -n "$slot" ]; then + out=$("$VC" --text --non-interactive --volume-properties --slot="$slot" 2>&1) + else + out=$("$VC" --text --non-interactive --volume-properties "$volume" 2>&1) + fi + code=$? + ;; + auto-mount) + out=$(vc_run "$@" 2>&1) + code=$? + ;; + list-token-keyfiles) + if [ -z "$token_lib" ]; then + json_fail "No PKCS #11 library path is set. Set Timeouts → Security token library (example: /usr/lib/libykcs11.so). This LuCI app has no Settings > Security Tokens." + return 0 + fi + out=$("$VC" --text --non-interactive --token-lib="$token_lib" --list-token-keyfiles 2>&1) + code=$? + case $out in + *'Security Tokens'*) + out="No PKCS #11 library loaded from $token_lib. Set Timeouts → Security token library to a valid .so. This LuCI app has no Settings > Security Tokens." + ;; + esac + ;; + list-securitytoken-keyfiles) + out=$("$VC" --text --non-interactive --list-securitytoken-keyfiles 2>&1) + code=$? + ;; + list-emvtoken-keyfiles) + out=$("$VC" --text --non-interactive --list-emvtoken-keyfiles 2>&1) + code=$? + ;; + import-token-keyfiles) + out=$("$VC" "$@" --import-token-keyfiles 2>&1) + code=$? + ;; + export-token-keyfile) + out=$("$VC" "$@" --export-token-keyfile 2>&1) + code=$? + ;; + delete-token-keyfiles) + out=$("$VC" "$@" --delete-token-keyfiles 2>&1) + code=$? + ;; + *) + json_fail "unknown action" + return 0 + ;; + esac + + if [ "$code" -eq 0 ]; then + json_ok "$out" + else + json_fail "$out" + fi +} + +case "$1" in + list) + echo '{"status":{},"listdir":{"path":"str"},"listdev":{},"mkdir":{"path":"str"},"rm":{"path":"str","recursive":"str"},"job":{},"job_log":{},"job_abort":{},"job_dismount":{},"job_answer":{"answer":"str"},"tools":{},"pkg_install":{"packages":"str"},"run":{"action":"str","name":"str","volume":"str","mountpoint":"str","password":"str","new_password":"str","pim":"str","new_pim":"str","hash":"str","new_hash":"str","encryption":"str","filesystem":"str","fs_options":"str","keyfiles":"str","new_keyfiles":"str","protect_hidden":"str","protection_password":"str","protection_pim":"str","protection_hash":"str","protection_keyfiles":"str","slot":"str","size":"str","volume_type":"str","random_source":"str","token_lib":"str","token_pin":"str","mount_options":"str","auto_mount":"str","force":"str","quick":"str","no_size_check":"str","legacy_password_maxlength":"str","allow_insecure_mount":"str","fsck_auto":"str","backup_file":"str"}}' + ;; + call) + case "$2" in + status) cmd_status ;; + listdir) cmd_listdir ;; + listdev) cmd_listdev ;; + mkdir) cmd_mkdir ;; + rm) cmd_rm ;; + job) cmd_job ;; + job_log) cmd_job_log ;; + job_abort) cmd_job_abort ;; + job_dismount) cmd_job_dismount ;; + job_answer) cmd_job_answer ;; + tools) cmd_tools ;; + pkg_install) cmd_pkg_install ;; + run) cmd_run ;; + esac + ;; +esac diff --git a/applications/luci-app-veracrypt/root/usr/share/luci/menu.d/luci-app-veracrypt.json b/applications/luci-app-veracrypt/root/usr/share/luci/menu.d/luci-app-veracrypt.json new file mode 100644 index 000000000000..c736c7a00d27 --- /dev/null +++ b/applications/luci-app-veracrypt/root/usr/share/luci/menu.d/luci-app-veracrypt.json @@ -0,0 +1,13 @@ +{ + "admin/services/veracrypt": { + "title": "VeraCrypt", + "order": 70, + "action": { + "type": "view", + "path": "veracrypt" + }, + "depends": { + "acl": [ "luci-app-veracrypt" ] + } + } +} diff --git a/applications/luci-app-veracrypt/root/usr/share/rpcd/acl.d/luci-app-veracrypt.json b/applications/luci-app-veracrypt/root/usr/share/rpcd/acl.d/luci-app-veracrypt.json new file mode 100644 index 000000000000..28f1eb70219e --- /dev/null +++ b/applications/luci-app-veracrypt/root/usr/share/rpcd/acl.d/luci-app-veracrypt.json @@ -0,0 +1,20 @@ +{ + "luci-app-veracrypt": { + "description": "Grant access to VeraCrypt console operations and volume configuration", + "read": { + "ubus": { + "luci.veracrypt": [ "status", "listdir", "listdev", "job", "job_log", "tools" ] + }, + "uci": [ "veracrypt" ] + }, + "write": { + "ubus": { + "luci.veracrypt": [ + "status", "listdir", "listdev", "mkdir", "rm", "job", "job_log", + "job_abort", "job_dismount", "job_answer", "tools", "pkg_install", "run" + ] + }, + "uci": [ "veracrypt" ] + } + } +}