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" ]
+ }
+ }
+}