Compare commits

..

261 Commits

Author SHA1 Message Date
0604745f82 feat: v1.9.20, simple-encrypt/simple-decrypt support iterations 2026-01-01 21:16:10 +08:00
1f4db9d1b0 feat: update gpg 2025-12-28 20:45:45 +08:00
cdec79e4dc feat: v1.9.18 2025-12-28 20:18:39 +08:00
3812001474 feat: updates 2025-10-18 14:45:50 +08:00
3708781390 feat: v1.9.18 2025-10-18 14:32:06 +08:00
a6397fc45a feat: v1.9.17 2025-10-17 19:20:41 +08:00
0e6b590f32 feat: udpate README 2025-10-16 07:08:14 +08:00
b0ee1f2c59 feat: v1.9.16 2025-10-14 23:38:22 +08:00
c176021c81 feat: v1.9.15, Secure Enclave ML-KEM-768, ML-KEM-1024 encrypt/decrypt 2025-09-27 20:51:56 +08:00
d75c589b66 feat: add keymlkem 2025-09-26 23:44:31 +08:00
0c4663f7f0 feat: updates 2025-09-26 23:28:41 +08:00
c446a52462 feat: encrypt ML-KEM-768&1024 2025-09-26 23:25:53 +08:00
9b0ecef9a0 feat: pending ML-KEM encryption and decryption 2025-09-26 01:13:37 +08:00
783b3e1962 feat: pending ML-KEM encryption and decryption 2025-09-26 01:13:24 +08:00
7b7878e2c1 feat: make clippy happy 2025-09-14 17:21:55 +08:00
403eaf1669 feat: v1.9.14, enhance temporary keys 2025-09-14 17:17:18 +08:00
75ed193d86 feaat: v1.9.13, add temporary key for simple encrypt 2025-09-14 15:29:07 +08:00
c8175e2654 feat: update readme, add TINY_ENCRYPT_ENV_EXTERNAL_COMMAND 2025-09-13 23:58:08 +08:00
7c752eab41 feat: update readme, add TINY_ENCRYPT_ENV_EXTERNAL_COMMAND 2025-09-13 23:57:39 +08:00
96ebf7f592 feat: udpate readme, add ext-p256, ext-p384 2025-09-13 23:53:46 +08:00
b18a5ec3e2 feat: v1.9.12, supports ext keys(p256, p384) 2025-09-13 23:17:00 +08:00
813b8b1b24 feat: v1.9.11 2025-09-13 22:30:13 +08:00
6a84ae7f5c feat: update USAGE 2025-08-25 00:07:59 +08:00
ff30fd42dd feat: v1.9.10 2025-08-24 23:29:24 +08:00
0ad8f83092 feat: v0.6.50 2025-08-24 23:01:11 +08:00
1741c335db feat: update dependencies 2025-08-24 16:17:47 +08:00
93cb3309bf feat: fix ask use default pin 2025-08-24 14:12:22 +08:00
6a24388a19 feat: v1.9.9 2025-08-24 13:53:33 +08:00
b94acf9c31 feat: v1.9.8 2025-08-24 12:34:25 +08:00
b91b29e22d feat: update readme 2025-07-27 11:11:07 +08:00
12e828732e feat: v1.9.7 2025-07-27 11:03:17 +08:00
d98005d799 feat: v1.9.7 2025-07-27 11:02:23 +08:00
e308809b20 feat: v1.9.7 2025-07-27 11:01:40 +08:00
920aa92b0e feat: v1.9.6 2025-07-27 10:43:42 +08:00
20c54350ee feat: v1.9.5 2025-07-27 10:32:07 +08:00
044daaad7d feat: v1.9.4, config support human JSON 2025-07-24 22:51:43 +08:00
f0f505bde3 feat: v1.9.3 2025-05-12 23:58:14 +08:00
4ad735d840 feat: v1.9.2, fix compile issue 2025-03-26 23:49:42 +08:00
d831b606cd feat: v1.9.1 2025-03-24 01:16:33 +08:00
84ee9d1b34 Merge pull request 'feat: v1.9.0, remove swiftc dependency' (#6) from use-swift-se-tool into master
Reviewed-on: #6
2025-03-24 00:36:07 +08:00
1b10c2a0c8 feat: v1.9.0, remove swiftc dependency 2025-03-24 00:29:29 +08:00
3c717a8f8d feat: updates, make clippy happy 2025-02-08 21:33:57 +08:00
a7802a3750 feat: v1.8.5, support simple PBKDF encryption 2025-01-26 00:22:16 +08:00
cc5178d1c1 feat: v1.8.4, load config from multipl locations 2025-01-13 00:37:48 +08:00
b21714cb4b feat: add zigbuilg 2025-01-12 23:55:36 +08:00
6635a0e468 feat: update readme 2024-12-28 23:47:48 +08:00
8b38982e61 feat: v1.8.3, fix lib issue 2024-12-15 16:55:44 +08:00
528889bcc7 feat: v1.8.2, fix compile issue 2024-12-15 16:34:07 +08:00
338017590f feat: update dependencies 2024-11-23 23:49:47 +08:00
08fa61c968 feat: update readme 2024-11-17 22:50:18 +08:00
75740a26a4 feat: update readme 2024-11-17 22:46:41 +08:00
85a7448291 feat: v1.8.1, add --direct-output for simple-encrypt/decrypt 2024-11-17 20:16:56 +08:00
cd9b3eb624 feat: updates readme 2024-11-17 11:45:36 +08:00
0aafaa7586 feat: updates 2024-11-17 11:36:46 +08:00
408b3e468b feat: install from git 2024-11-17 11:29:43 +08:00
fd0751d348 feat: use crates.io 2024-11-17 11:20:44 +08:00
096218fc78 feat: use crates.io 2024-11-17 11:17:17 +08:00
2a03dd45ce feat: use crates.io 2024-11-17 11:08:48 +08:00
3046626691 feat: use crates.io 2024-11-17 10:55:23 +08:00
f2388b11cf feat: hatter fork 2024-11-17 10:50:06 +08:00
90372941be feat: update argument output-type to output-format 2024-11-17 10:38:06 +08:00
b9e7b5af63 Merge pull request 'simple-encrypt-decrypt' (#4) from simple-encrypt-decrypt into master
Reviewed-on: #4
2024-11-17 10:32:36 +08:00
8e6e708c73 feat: v1.8.0, simple-encrypt and simple-decrypt 2024-11-17 10:30:39 +08:00
6a07360dc1 feat: pendig simple encrypt/decrypt 2024-11-16 21:59:36 +08:00
d6e1f96207 feat: v1.7.15, update dependencies 2024-11-15 23:16:00 +08:00
228c69731a feat: update dependencies 2024-11-09 23:50:36 +08:00
568022e655 feat: v1.7.14, support auto compress exts 2024-09-22 01:09:16 +08:00
ac727f082e feat: update dependencies 2024-07-28 21:31:16 +08:00
623e657211 feat: update dependencies 2024-06-16 10:44:31 +08:00
598dbc38c7 feat: v1.7.13 2024-06-16 10:37:40 +08:00
f74a81d4ca feat: v1.7.12, add PEM output for file encryption 2024-06-14 15:47:54 +08:00
0309f77ed7 feat: update dependencies 2024-03-24 17:29:01 +08:00
1340f73bad feat: 1.7.11, update dependencies, change default features 2024-03-17 12:48:01 +08:00
2067faec8e feat: 1.7.11, update dependencies, change default features 2024-03-17 12:46:34 +08:00
de675d215a feat: v1.7.10, update dependencies 2024-03-02 11:54:10 +08:00
3127dfab0a feat: update dependencies 2024-02-24 22:19:06 +08:00
0022181bd6 feat: update dependencies 2024-02-17 17:42:02 +08:00
af1678f67a feat: v1.7.9, meta size 2024-01-20 15:26:12 +08:00
80ce9fcece v1.7.8, update config display 2024-01-14 22:03:26 +08:00
62feb2e246 feat: updates 2024-01-13 20:43:04 +08:00
e31f6d49b7 feat: v1.7.7, support exec-env comment 2024-01-13 11:02:01 +08:00
784765fbb9 feat: v1.7.6, update denpendencies, fix diaguer ctrl-c do not show cursor 2024-01-13 10:39:22 +08:00
6283c28fff feat: 1.7.5, supports dialoguer 2024-01-12 23:53:12 +08:00
c093f865ab feat: update comments 2024-01-11 00:26:13 +08:00
27c6bee4ea feat: v1.7.4, add slient mod for exec-env 2024-01-11 00:24:39 +08:00
2bf0614854 feat: v1.7.3, supports pinentry 2024-01-10 00:44:27 +08:00
2bcb0fe5c4 feat: v1.7.3, supports pinentry 2024-01-10 00:35:38 +08:00
2e53130ead feat: v1.7.2, support store secret to not default keychain 2023-12-30 11:50:46 +08:00
3ce150db72 feat: update util_gpg 2023-12-28 23:44:30 +08:00
3e2a3cc7f3 feat: keychain-name, but tests not passed 2023-12-28 23:00:29 +08:00
61853889ca feat: update readme, add gpg 2023-12-27 23:29:45 +08:00
b7d3f77385 feat: makes clippy happy 2023-12-27 00:12:43 +08:00
004df06df2 feat: v1.7.0, support GPG encrypt and decrypt 2023-12-27 00:09:55 +08:00
e60f491801 feat: add gpg command encrypt/decrypt support 2023-12-26 23:31:42 +08:00
5f04aa5783 feat: v1.6.1, update dependencies, optimize code 2023-12-24 09:26:40 +08:00
0c5dbc7cc3 feat: code style 2023-12-23 16:25:26 +08:00
d19c9a48f1 feat: profile ALL filter all envelops 2023-12-23 16:10:57 +08:00
def76d1957 feat: import code style 2023-12-23 15:55:23 +08:00
171cd2d2a4 feat: fix compile issue, code style 2023-12-23 15:53:26 +08:00
a3d101a405 feat: update wk 2023-12-23 15:25:05 +08:00
9b73b4ecc1 feat: update readme 2023-12-23 10:48:02 +08:00
e7b4f022bf feat: v1.6.0, support kyber1024 2023-12-23 10:34:10 +08:00
df5ba3c53d feat: v1.5.4, update dependencies 2023-12-20 23:52:12 +08:00
0b09630635 feat: v1.5.3, environment support vec string 2023-12-19 23:25:11 +08:00
343a6f5fac feat: v1.5.2, fix edit file issue, add prefixes 2023-12-15 21:15:22 +08:00
a7398a2578 feat: fix clippy 2023-12-14 23:51:37 +08:00
9de9af0cde feat: v1.5.1, secure enclave key can store in keychain or not 2023-12-14 23:47:27 +08:00
5bdc4c69e6 feat: v1.5.0, store secure enclave private key to keychain 2023-12-14 21:56:30 +08:00
c30d061f53 feat: update readme 2023-12-12 23:24:15 +08:00
bafb12bdf7 feat: update readme 2023-12-12 23:21:24 +08:00
5822cf9b7f feat: update readme 2023-12-12 23:16:34 +08:00
7f1abf7dea feat: v1.4.5 2023-12-12 23:07:10 +08:00
f9a5f39330 feat: refactor naming 2023-12-10 15:16:45 +08:00
fc3275d978 feat: optimize encrypt 2023-12-10 15:05:15 +08:00
26c5a25759 feat: optimize code 2023-12-10 14:37:26 +08:00
e7a9877cad feat: optimize info 2023-12-10 14:31:21 +08:00
fd98c27462 feat: optimize info 2023-12-10 14:22:08 +08:00
0189b5a765 feat: update parse pem 2023-12-10 14:17:28 +08:00
90ecfe76ba feat: update parse pem 2023-12-10 14:16:20 +08:00
2c2f623df4 feat: add auto select key ids 2023-12-10 14:08:02 +08:00
c597a87557 feat: optimize decrypt 2023-12-10 13:44:49 +08:00
2ccb4f213b feat: optimize decrypt 2023-12-10 13:41:06 +08:00
33e08e657d feat: optimize decrypt 2023-12-10 13:33:42 +08:00
880b5be94e feat: v1.4.4, support config environment 2023-12-10 13:01:10 +08:00
7ee38b8ac5 feat: v1.4.3, init static x25519 when exists 2023-12-10 12:32:46 +08:00
c393302d6c feat: v1.4.2, add latest user agent in meta 2023-12-10 10:49:02 +08:00
72d89b32ab feat: v1.4.2, add latest user agent in meta 2023-12-10 10:48:16 +08:00
c1debe5262 feat: v1.4.1, optimize config outputs 2023-12-10 10:37:04 +08:00
54ac065bd3 feat: v1.4.1, optimize config outputs 2023-12-10 10:35:08 +08:00
f5bd348d5b feat: v1.4.1, optimize config outputs 2023-12-10 10:23:47 +08:00
c49dfcf261 feat: v1.4.0, support PIV RSA key 2023-12-10 10:00:37 +08:00
d0218ee233 feat: v1.4.0, support PIV RSA key 2023-12-10 09:58:08 +08:00
b15b9a5b32 feat: optimize code 2023-12-10 00:15:15 +08:00
2845e5155e feat: optimize code 2023-12-10 00:10:17 +08:00
34b60f6929 feat: v1.3.1, optimize code 2023-12-10 00:07:01 +08:00
68d3c07767 feat: optimize code 2023-12-09 22:05:29 +08:00
0a815dbbdf feat: optimize code 2023-12-09 22:02:31 +08:00
993f10ba71 feat: v1.3.0, support init-piv 2023-12-09 21:56:00 +08:00
af7bf60869 feat: v1.2.2, optimize codes 2023-12-09 20:33:18 +08:00
9b735f8e48 feat: update wk 2023-12-09 20:14:59 +08:00
2d29e6eaed feat: update readme 2023-12-09 19:14:19 +08:00
baafa7aa31 feat: rm unused files 2023-12-09 19:04:56 +08:00
df36022203 feat: rm swift-rs/example 2023-12-09 18:59:46 +08:00
5e73466400 feat: update dependencies 2023-12-09 18:54:35 +08:00
115e0f7094 feat: v1.2.1, optimize naming 2023-12-09 18:53:33 +08:00
d75a88c90c feat: update readme 2023-12-09 16:38:41 +08:00
36fcaab6be feat: udpate swift lib 2023-12-09 15:16:25 +08:00
42bc09fe07 feat: v1.2.0, support macos secure enclave 2023-12-09 13:58:30 +08:00
a8b2fc62b8 feat: generate se keypair 2023-12-09 12:49:03 +08:00
2034db589a feat: secure enclave is on going 2023-12-09 11:43:21 +08:00
fa1cd80fc8 feat: secure enclave 2023-12-09 10:45:23 +08:00
215b75d6da feat: add crate swift-rs 2023-12-09 10:17:41 +08:00
d36c06f461 feat: libse 2023-12-09 00:24:59 +08:00
6b132b800a feat: add libse 2023-12-08 23:42:17 +08:00
a60bf5c74a feat: pending support key-256 (secure enclave) 2023-12-08 23:23:53 +08:00
43b43ba055 feat: feature secure-enclave 2023-12-08 23:10:17 +08:00
be65cfb06c feat: pending support key-256 (secure enclave) 2023-12-08 23:06:49 +08:00
24c6d3bbad feat: rename vn 2023-12-08 23:00:41 +08:00
638e40448a feat: v1.1.2, support readonly for secure editor 2023-12-08 22:58:54 +08:00
bd586130fb feat: v1.1.2, support readonly for secure editor 2023-12-08 22:56:57 +08:00
bae6c13582 feat: v1.1.1, fix2 2023-12-08 21:53:26 +08:00
92e6808820 feat: v1.1.1, fix 2023-12-08 21:48:29 +08:00
fd7e8d35a6 feat: v1.1.0, add static x25519 support 2023-12-08 21:36:03 +08:00
883ed0918b feat: v1.0.2, support --create to create empty encrypted file 2023-12-03 15:20:18 +08:00
fc4ec8d41c feat: v1.0.1, fix typo 2023-12-02 15:32:15 +08:00
76b8f93ddd feat: v1.0.0, suport exec-env 2023-12-02 15:26:15 +08:00
0bf4ef2e6b feat: 0.9.1, update dependencies 2023-12-02 13:11:53 +08:00
9e36725908 feat: v0.9.0, support secure editor 2023-12-02 12:20:28 +08:00
1e2f13e0b8 feat: v0.8.2, support secure editor 2023-12-01 23:49:53 +08:00
ae84bbd13b feat: improve decrypt 2023-12-01 21:52:26 +08:00
4966ddf72b feat: v0.8.0, decrypt --edit works 2023-12-01 00:23:20 +08:00
4b426f7b13 feat: 0.7.2, remove direct sha256 dependency 2023-11-12 23:19:34 +08:00
9c5b28eb2a feat: update dependencies 2023-11-12 23:08:28 +08:00
4306825706 feat: optimize code 2023-11-05 21:22:15 +08:00
42a7a0fb28 feat: optimize code 2023-11-05 21:12:01 +08:00
5ec12237d9 feat: optimize 2023-11-05 00:36:34 +08:00
699474cad4 feat: update comments 2023-11-05 00:19:39 +08:00
e6b1ec8a86 feat: optimize code 2023-11-04 23:35:54 +08:00
6279e8e16b feat: update dependencies 2023-11-04 23:24:27 +08:00
d8ae0ad9db feat: add justfile 2023-10-29 21:31:56 +08:00
3a3ebd64bf feat: v0.7.1, compiles without smart card(encrypt only) 2023-10-29 17:01:38 +08:00
25de8ea682 feat: comments 2023-10-29 00:43:27 +08:00
9e46d11e89 feat: v0.7.0, add direct decrypt 2023-10-29 00:38:00 +08:00
fa5d01a7d5 feat: optimize code 2023-10-28 18:08:32 +08:00
a6d78928d9 feat: optimize code 2023-10-28 18:04:20 +08:00
e478e43e12 feat: optimize code 2023-10-28 17:59:35 +08:00
231bf28240 feat: optimize code 2023-10-28 17:55:42 +08:00
8aec9ad9a3 feat: optimize code 2023-10-28 17:54:39 +08:00
6161e2d293 feat: optimize code 2023-10-28 17:52:03 +08:00
67465d584e feat: optimize code 2023-10-28 17:49:16 +08:00
e355d0f03d feat: optimize code 2023-10-28 17:47:38 +08:00
9d3de9c8f0 feat: optimize 2023-10-28 17:43:20 +08:00
84eb5c789b feat: clean code 2023-10-28 17:09:42 +08:00
20b0fe1ea2 feat: optimize 2023-10-28 16:47:08 +08:00
89ed3c26ed feat: update dependencies, optimize 2023-10-28 16:41:07 +08:00
373b3bcefc feat: v0.6.0 2023-10-28 11:38:16 +08:00
9ec0dba5b3 feat: fix clippy 2023-10-28 09:11:04 +08:00
3d59680f0c feat: formatting use 2023-10-28 00:16:14 +08:00
80e3a3540e feat: v0.5.4, many updates 2023-10-28 00:10:27 +08:00
19269bf6de feat: print ln 2023-10-25 23:18:14 +08:00
c05cf1a7cf feat: v0.5.3, fix compress issue, supports env TINY_ENCRYPT_DEFAULT_ALGORITHM 2023-10-25 23:16:20 +08:00
d5cb25cc6a feat: fix clippy 2023-10-25 07:21:27 +08:00
f039c183b2 feat: 0.5.2, add --split-print 2023-10-25 01:20:37 +08:00
dcda185512 feat: fix x25519 decrypt 2023-10-22 22:53:53 +08:00
0fa22f7f39 feat: update zeroize 2023-10-22 22:46:25 +08:00
0f02edc6f6 feat: add zeroize 2023-10-22 22:40:47 +08:00
2a7d28372e feat: v0.5.1, envelop supports ChaCha20/Poly1305 2023-10-22 22:31:26 +08:00
5dc1927cc9 feat: v0.5.0, supports CahCha20/Poly1305 2023-10-22 19:19:54 +08:00
1012b33f31 feat: --info 2023-10-22 19:09:07 +08:00
bb07aec896 feat: v0.5.0, supports ChaCha20/Poly1305 2023-10-22 19:05:38 +08:00
83464dfb28 feat: pedning add chacha20-poly1305 2023-10-22 17:08:39 +08:00
b3359d205b feat: rename macOS 2023-10-19 23:26:57 +08:00
a66babb828 feat: update version 2023-10-19 23:24:03 +08:00
94a6cf18b3 feat: v0.4.4, add radio outputs when compressed 2023-10-19 21:55:19 +08:00
aa6c4f325d feat: fix compile imcompat warnning 2023-10-18 00:30:26 +08:00
452cb2b666 feat: update dependencies 2023-10-17 23:56:40 +08:00
6e00247bf6 feat: optimize --skip-decrypt-file 2023-10-17 23:51:48 +08:00
186330fe2c feat: v0.4.3, add --digest-file for decrypt 2023-10-17 22:27:25 +08:00
ddd3ac3b2d feat: optimize decrypt 2023-10-17 00:40:41 +08:00
17fae72d91 feat: optimize decrypt 2023-10-17 00:06:23 +08:00
e0b6df7c46 feat: update do_write_file_out 2023-10-17 00:03:07 +08:00
90040bbf8e feat: update do_write_file_out 2023-10-16 23:58:50 +08:00
9db0cec508 feat: v0.4.2, add --direct-print 2023-10-16 23:57:24 +08:00
b4d9b4692b feat: update envelop format 2023-10-16 00:02:52 +08:00
ab2e850435 feat: optimize info outputs 2023-10-15 22:56:24 +08:00
f281cc15a2 feat:update info 2023-10-15 22:36:03 +08:00
7d0115541f feat: update progress 2023-10-15 22:22:16 +08:00
47a0625dcc feat: optimize code 2023-10-15 21:43:02 +08:00
eda2d94d21 feat: v0.4.1, enchance config outputs 2023-10-15 19:18:33 +08:00
0ab41c87fb feat: upate code 2023-10-15 18:07:46 +08:00
cb72baebee feat: v0.4.0, add progress bar 2023-10-15 17:56:23 +08:00
6f70e7c83e feat: clean code 2023-10-15 17:24:00 +08:00
bec7306918 feat: clean code 2023-10-15 17:22:34 +08:00
dda040a1b3 feat: udpate code 2023-10-15 17:18:06 +08:00
bae6ca828d feat: fix clippy 2023-10-15 17:01:07 +08:00
66e93106fe feat: update code 2023-10-15 16:56:03 +08:00
a37d5b8d62 feat: update config 2023-10-15 16:54:29 +08:00
93b957a4c3 feat: display sid or kid 2023-10-15 14:16:14 +08:00
b0af535aa3 feat: v0.3.4, encrypt supports --key-filter 2023-10-15 13:55:14 +08:00
cd359bb6ac feat: optimize code 2023-10-15 12:55:45 +08:00
03ec9a0fe0 feat: optimize code 2023-10-15 11:53:40 +08:00
1a3c670bad feat: optimize code 2023-10-15 11:53:29 +08:00
12aa3b48d2 feat: update comments 2023-10-15 11:35:47 +08:00
b268d88e3c feat: v0.3.3, optimize code 2023-10-15 11:29:31 +08:00
2624b0fa64 feat: v0.3.2, store file create/modified time and restore 2023-10-15 11:06:55 +08:00
161b901829 feat: update spec link 2023-10-13 21:54:26 +08:00
7fa49bdf05 feat: fix clippy 2023-10-13 21:49:59 +08:00
d1cc03cf2f feat: fix clippy 2023-10-13 21:41:47 +08:00
c9774d7542 feat: fix clippy 2023-10-13 08:47:38 +08:00
c873a52ac6 feat: 0.3.1, update encrypts 2023-10-12 23:42:03 +08:00
ce7911819c feat: fix error message 2023-10-12 23:33:06 +08:00
9cae4e987a feat: 0.3.0, supports meta compress, encrypted meta 2023-10-12 22:38:50 +08:00
097cde6b9a feat: v0.2.5, show decrytped encrypted-message 2023-10-11 01:10:07 +08:00
6d1d6ab085 feat: v0.2.5, show decrytped encrypted-message 2023-10-11 01:09:20 +08:00
c35fef7bf1 feat: sort config outputs 2023-10-11 00:07:47 +08:00
a68f5e0e00 feat: v0.2.4, add config subcommand 2023-10-10 22:28:05 +08:00
fe226d9b75 feat: update readme 2023-10-10 01:10:14 +08:00
a64649451c feat: v0.2.3, supports p384 2023-10-10 00:59:59 +08:00
308c6561bc feat: find kid first arg 2023-10-09 22:28:16 +08:00
6833a1a7d3 feat: v0.2.2, auto find PIV slot id 2023-10-09 00:42:30 +08:00
196d500c51 feat: v0.2.1, add version 2023-10-08 22:29:06 +08:00
47 changed files with 6258 additions and 1786 deletions

10
.gitignore vendored
View File

@@ -1,3 +1,5 @@
.swiftpm/
.build/
_tinyencrypt_config-rs.json _tinyencrypt_config-rs.json
*.tinyenc *.tinyenc
# ---> Rust # ---> Rust
@@ -7,6 +9,11 @@ debug/
target/ target/
.idea/ .idea/
libse.a
se.swiftdoc
se.swiftmodule
se.swiftsourceinfo
# These are backup files generated by rustfmt # These are backup files generated by rustfmt
**/*.rs.bk **/*.rs.bk
@@ -20,7 +27,8 @@ target/
.LSOverride .LSOverride
# Icon must end with two \r # Icon must end with two \r
Icon Icon
# Thumbnails # Thumbnails
._* ._*

2260
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,35 +1,62 @@
[package] [package]
name = "tiny-encrypt" name = "tiny-encrypt"
version = "0.2.0" version = "1.9.20"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
description = "A simple and tiny file encrypt tool" description = "A simple and tiny file encrypt tool"
repository = "https://git.hatter.ink/hatter/tiny-encrypt-rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
default = ["decrypt", "macos", "smartcard"]
full-features = ["decrypt", "macos", "smartcard"]
decrypt = ["smartcard"]
smartcard = ["openpgp-card", "openpgp-card-pcsc", "yubikey"]
macos = ["security-framework"]
[dependencies] [dependencies]
aes-gcm-stream = "0.2.0" aes-gcm-stream = "0.2"
base64 = "0.21.0" base64 = "0.22"
chrono = "0.4.23" chacha20-poly1305-stream = "0.1"
clap = { version = "4.1.4", features = ["derive"] } chrono = "0.4"
flate2 = "1.0.27" clap = { version = "4.4", features = ["derive"] }
hex = "0.4.3" flate2 = "1.0"
openpgp-card = "0.3.7" fs-set-times = "0.20"
openpgp-card-pcsc = "0.3.0" hex = "0.4"
p256 = { version = "0.13.2", features = ["pem", "ecdh", "pkcs8"] } indicatif = "0.18"
rand = "0.8.5" openpgp-card = { version = "0.3", optional = true }
reqwest = { version = "0.11.14", features = ["blocking", "rustls", "rustls-tls"] } openpgp-card-pcsc = { version = "0.3", optional = true }
rpassword = "7.2.0" p256 = { version = "0.13", features = ["pem", "ecdh", "pkcs8"] }
rsa = { version = "0.9.2", features = ["pem"] } p384 = { version = "0.13", features = ["pem", "ecdh", "pkcs8"] }
rust_util = "0.6.42" rand = "0.8"
serde = { version = "1.0.152", features = ["derive"] } # reqwest = { version = "0.11", features = ["blocking", "rustls", "rustls-tls"] }
serde_json = "1.0.93" rpassword = "7.3"
sha256 = "1.4.0" rsa = { version = "0.9", features = ["pem"] }
simpledateformat = "0.1.4" rust-crypto-hatter-fork = "0.2"
x25519-dalek = "2.0.0" rust_util = "0.6"
x509-parser = "0.15.1" security-framework = { version = "3.0", features = ["OSX_10_15"], optional = true }
yubikey = { version = "0.8.0", features = ["untested"] } serde = { version = "1.0", features = ["derive"] }
zeroize = "1.6.0" serde_json = "1.0"
simpledateformat = "0.1"
tabled = "0.20"
x25519-dalek = { version = "2.0", features = ["static_secrets", "getrandom"] }
x509-parser = "0.17"
yubikey = { version = "0.8", features = ["untested"], optional = true }
zeroize = "1.7"
spki = "0.7"
pqcrypto-kyber = "0.8"
pqcrypto-traits = "0.3"
pinentry = "0.6"
secrecy = "0.10"
dialoguer = "0.11"
ctrlc = "3.4"
swift-secure-enclave-tool-rs = "1.0"
json5 = "0.4"
external-command-rs = "0.1"
percent-encoding = "2.3"
ml-kem = { version = "0.2.1", features = ["zeroize"] }
zeroizing-alloc = "0.1.0"
[profile.release] [profile.release]
codegen-units = 1 codegen-units = 1

125
README.md
View File

@@ -1,32 +1,74 @@
# tiny-encrypt-rs # tiny-encrypt-rs
**IMPORTANT**: To use tiny-encrypt, a Yubikey(https://www.yubico.com/products/) or CanoKey(https://www.canokeys.org/) is > [!IMPORTANT]
required, the Key NEED supports PIV or OpenPGP. > To use tiny-encrypt, a Yubikey(https://www.yubico.com/products/) or MacBook with Secure Enclave get the
best security effect, the key MUST support PIV or OpenPGP.
![](https://cdn.hatter.ink/doc/7684_4DB4452911E2A25AB993429AA7FFCD65/yubikey-5-family.png) ![](https://cdn.hatter.ink/doc/7684_4DB4452911E2A25AB993429AA7FFCD65/yubikey-5-family.png)
Tiny encrypt for Rust Tiny Encrypt written in Rust Programming Language
Specification: [Tiny Encrypt Spec V1.1](https://github.com/OpenWebStandard/tiny-encrypt-format-spec/blob/main/TinyEncryptSpecv1.1.md)
> Tiny encrypt rs is a Rust implementation of Tiny encrypt java https://git.hatter.ink/hatter/tiny-encrypt-java <br> > Tiny encrypt rs is a Rust implementation of Tiny encrypt java https://git.hatter.ink/hatter/tiny-encrypt-java <br>
> Tiny encrypt spec see: https://github.com/OpenWebStandard/tiny-encrypt-format-spec > Tiny encrypt spec see: https://github.com/OpenWebStandard/tiny-encrypt-format-spec
Repository address: https://git.hatter.ink/hatter/tiny-encrypt-rs mirror https://github.com/jht5945/tiny-encrypt-rs Repository address: https://git.hatter.ink/hatter/tiny-encrypt-rs mirror https://github.com/jht5945/tiny-encrypt-rs
Set default encryption algorithm:
```shell
export TINY_ENCRYPT_DEFAULT_ALGORITHM='AES' # or CHACHA20
```
Compile only encrypt:
```shell
cargo build --release --no-default-features
```
Install from git:
```shell
cargo install --git https://git.hatter.ink/hatter/tiny-encrypt-rs.git --features full-features
```
Edit encrypted file:
```shell
tiny-encrypt decrypt --edit-file sample.txt.tinyenc
```
Read environment `EDITOR` or `SECURE_EDITOR` to edit file, `SECURE_EDITOR` write encrypted file to temp file.
Secure editor command format:
```shell
$SECURE_EDITOR <temp-file-name> "aes-256-gcm" <temp-key-hex> <temp-nonce-hex>
```
<br> <br>
Encrypt config `~/.tinyencrypt/config-rs.json`: Encrypt config `~/.tinyencrypt/config-rs.json`:
```json ```json
{ {
"environment": {
"TINY_ENCRYPT_DEFAULT_ALGORITHM": "AES or CHACHA20"
},
"namespaces": {
"name": "/Users/example/.name"
},
"envelops": [ "envelops": [
{ {
"type": "pgp", "type": "pgp-rsa",
"kid": "KID-1", "kid": "KID-1",
"desc": "this is key 001", "desc": "this is key 001",
"publicPart": "----- BEGIN PUBLIC KEY ..." "publicPart": "----- BEGIN PUBLIC KEY ..."
}, },
{ {
"type": "ecdh", "type": "piv-p256",
"kid": "KID-2", "kid": "KID-2",
"desc": "this is key 002", "desc": "this is key 002",
"publicPart": "04..." "publicPart": "04..."
@@ -37,14 +79,60 @@ Encrypt config `~/.tinyencrypt/config-rs.json`:
"KID-1", "KID-1",
"KID-2" "KID-2"
], ],
"leve2": [ "l2,leve2": [
"KID-2" "KID-2"
] ]
} }
} }
``` ```
Smart Card(Yubikey) protected ECDH Encryption description: <br>
Kyber1024 usage:
Generate `static-kyber1024` keypair:
```shell
$ tiny-encrypt -K -a kyber1024 -n keyname
[OK ] Keychain name: keyname
[OK ] Public key : a731b5032194c3d2ad01f36d64e859ca9738595c21aa19c852dac22f4...
[INFO ] Config envelop:
{
"type": "static-kyber1024",
"sid": "keyname",
"kid": "keychain:a731b5032194c3d2ad01f36d64e859ca9738595c21aa19c852dac22f411036c7",
"desc": "Keychain static",
"args": [
"keychain::tiny-encrypt:keyname"
],
"publicPart": "a731b5032194c3d2ad01f36d64e859ca9738595c21aa19c852dac22f411036c..."
}
```
Then write file `~/.tinyencrypt/config-rs.json`.
Last, config key id to profile.
Supported PKI encryption types:
| Type | Algorithm | Description |
|------------------|---------------------|--------------------------------------------------------|
| pgp-rsa | PKCS1-v1.5 | OpenPGP Encryption Key (Previous `pgp`) |
| pgp-x25519 | ECDH(X25519) | OpenPGP Encryption Key |
| gpg | OpenPGP | GnuPG Command |
| static-x25519 | ECDH(X25519) | Key Stored in macOS Keychain Access |
| static-kyber1024 | Kyber1024 | Key Stored in macOS Keychain Access |
| piv-p256 | ECDH(secp256r1) | PIV Slot (Previous `ecdh`) |
| piv-p384 | ECDH(secp384r1) | PIV Slot (Previous `ecdh-p384`) |
| key-p256 | ECDH(secp256r1) | Key Stored in macOS Secure Enclave (using P256) |
| key-mlkem768 | ML-KEM(ML-KEM-768) | Key Stored in macOS Secure Enclave (using ML-KEM-768) |
| key-mlkem1024 | ML-KEM(ML-KEM-1024) | Key Stored in macOS Secure Enclave (using ML-KEM-1024) |
| ext-p256 | ECDH(secp256r1) | Key Protected by External Command |
| ext-p384 | ECDH(secp384r1) | Key Protected by External Command |
| ext-mlkem768 | ML-KEM(ML-KEM-768) | Key Protected by External Command |
| ext-mlkem1024 | ML-KEM(ML-KEM-1024) | Key Protected by External Command |
| piv-rsa | PKCS1-v1.5 | PIV Slot |
Smart Card(Yubikey) protected ECDH Encryption description as below:
```text ```text
┌───────────────────┐ ┌───────────────────────────┐ ┌───────────────────┐ ┌───────────────────────────┐
@@ -67,3 +155,26 @@ Smart Card(Yubikey) protected ECDH Encryption description:
Decrypt using derived key from restored Shared Secret Decrypt using derived key from restored Shared Secret
``` ```
Environment
| KEY | Comment |
|----------------------------------|----------------------------------------------------------------|
| TINY_ENCRYPT_CONFIG_FILE | Config file |
| TINY_ENCRYPT_DEFAULT_ALGORITHM | Encryption algorithm, `aes` or `chacha20` |
| TINY_ENCRYPT_DEFAULT_COMPRESS | File compress, `1` or `on`, default `false` |
| TINY_ENCRYPT_NO_PROGRESS | Do not display progress bar |
| TINY_ENCRYPT_NO_DEFAULT_PIN_HINT | Do not display default PIN hint |
| TINY_ENCRYPT_USE_DIALOGUER | Use dialoguer |
| TINY_ENCRYPT_PIN | PIV Card PIN |
| TINY_ENCRYPT_KEY_ID | Default Key ID |
| TINY_ENCRYPT_AUTO_SELECT_KEY_IDS | Auto select Key IDs |
| TINY_ENCRYPT_AUTO_COMPRESS_EXTS | Auto compress file exts |
| TINY_ENCRYPT_PIN_ENTRY | PIN entry command cli |
| TINY_ENCRYPT_EXTERNAL_COMMAND | External command cli |
| SECURE_EDITOR | Secure Editor [\[OWS RFC6\]](https://openwebstandard.org/rfc6) |
| EDITOR | Editor (Plaintext) |
Alternative environment setup:
```shell
~/.config/envs/ENV_VARIABLE_NAME <--> File Content
```

View File

@@ -133,5 +133,10 @@ tiny-encrypt -e [-p Profile] [-x] [-L 6] [-1] [-R] [-c Comment] [-C EncryptedCom
tiny-encrypt -d [-p PIN] [-s Slot] [-R] FILENAMES tiny-encrypt -d [-p PIN] [-s Slot] [-R] FILENAMES
``` ```
## 5 Generate key(s)
### 5.1 Generate SE based key(s)
```shell
tiny-encrypt init-keychain -S -C biometry-current-set -E --key-name <key-name>
```

34
justfile Normal file
View File

@@ -0,0 +1,34 @@
_:
@just --list
# publish
publish:
cargo publish --registry crates-io
# Install local
install:
cargo install --path .
# Default build release
build:
cargo build --release
# Build release without features
build-no-features:
cargo build --release --no-default-features
# Build linux musl release without features via zig
build-linux-musl-with-zig:
cargo zigbuild --release --target x86_64-unknown-linux-musl --no-default-features
# Lint code
lint:
cargo clippy
# Try build all
try-build-all:
cargo build --no-default-features
cargo build --no-default-features --features smartcard
cargo build --no-default-features --features decrypt
cargo build --no-default-features --features macos
cargo build

View File

@@ -1,15 +0,0 @@
use openpgp_card_pcsc::PcscBackend;
use rust_util::{opt_result, opt_value_result, simple_error, warning, XResult};
pub fn get_card() -> XResult<PcscBackend> {
let card_list = opt_result!(
PcscBackend::cards(None), "Read OpenPGP card list failed: {}"
);
if card_list.is_empty() {
return simple_error!("Cannot find any card");
}
if card_list.len() > 1 {
warning!("Find {} OpenPGP cards, will use first card", card_list.len());
}
Ok(opt_value_result!(card_list.into_iter().next(), "SHOULD NOT HAPPEN, CANNOT FIND ANY CARD"))
}

194
src/cmd_config.rs Normal file
View File

@@ -0,0 +1,194 @@
use std::cmp::Ordering;
use std::collections::HashMap;
use clap::Args;
use rust_util::{iff, information, warning, XResult};
use tabled::{Table, Tabled};
use tabled::settings::Style;
use crate::config::TinyEncryptConfig;
use crate::temporary_key::serialize_config_envelop;
use crate::util_envelop;
#[derive(Tabled, Eq)]
struct ConfigProfile {
profiles: String,
keys: String,
}
impl PartialEq<Self> for ConfigProfile {
fn eq(&self, other: &Self) -> bool {
self.profiles.eq(&other.profiles)
}
}
impl Ord for ConfigProfile {
fn cmp(&self, other: &Self) -> Ordering {
self.profiles.cmp(&other.profiles)
}
}
impl PartialOrd for ConfigProfile {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
#[derive(Tabled)]
pub struct ConfigEnvelop {
pub r#type: String,
pub sid: String,
pub kid: String,
pub desc: String,
pub args: String,
}
#[derive(Debug, Args)]
pub struct CmdConfig {
/// Show KID
#[arg(long)]
pub show_kid: bool,
/// JSON output
#[arg(long)]
pub json: bool,
/// Temporary key output
#[arg(long)]
pub temporary_key: bool,
/// Hide __all__
#[arg(long)]
pub hide_all: bool,
/// Encryption profile (use default when --key-filter is assigned)
#[arg(long, short = 'p')]
pub profile: Option<String>,
/// Encryption key filter (key_id or type:TYPE(e.g. type:piv-p256, type:piv-p384, type:pgp-*), multiple joined by ',', ALL for all)
#[arg(long, short = 'k')]
pub key_filter: Option<String>,
}
pub fn config(cmd_config: CmdConfig) -> XResult<()> {
let config = TinyEncryptConfig::load_default(&None)?;
if cmd_config.json {
let mut config = config;
config.includes = None;
if let Some(profiles) = &mut config.profiles {
profiles.remove("__all__");
}
println!("{}", serde_json::to_string_pretty(&config)?);
return Ok(());
}
if cmd_config.temporary_key {
let envelops = if cmd_config.profile.is_some() || cmd_config.key_filter.is_some() {
config.find_envelops(&cmd_config.profile, &cmd_config.key_filter)?
} else {
config.find_envelops(&None, &None)?
};
for envelop in envelops {
let k = serialize_config_envelop(envelop);
println!("{}", k);
}
return Ok(());
}
if cmd_config.profile.is_some() || cmd_config.key_filter.is_some() {
return config_key_filter(&cmd_config, &config);
}
config_profiles(&cmd_config, &config)
}
fn config_key_filter(cmd_version: &CmdConfig, config: &TinyEncryptConfig) -> XResult<()> {
let envelops = config.find_envelops(&cmd_version.profile, &cmd_version.key_filter)?;
if envelops.is_empty() { warning!("Found no envelops"); }
information!("Found {} envelops", envelops.len());
let mut config_envelops = vec![];
for envelop in envelops {
let hardware_security_mark = match envelop.r#type.is_hardware_security() {
None => " ?",
Some(hardware_security) => iff!(hardware_security, " *", "")
};
config_envelops.push(ConfigEnvelop {
r#type: format!("{}{}", envelop.r#type.get_name(), hardware_security_mark),
sid: strip_field(&envelop.sid.as_ref().map(ToString::to_string).unwrap_or_else(|| "-".to_string()), 25),
kid: strip_field(&envelop.kid, 40),
desc: strip_field(&envelop.desc.as_ref().map(ToString::to_string).unwrap_or_else(|| "-".to_string()), 40),
args: strip_field(&envelop.args.as_ref().map(|a| format!("[{}]", a.join(", "))).unwrap_or_else(|| "-".to_string()), 20),
});
}
let mut table = Table::new(config_envelops);
table.with(Style::sharp());
println!("{}", table);
println!("> Type with * is hardware security");
Ok(())
}
fn strip_field(kid: &str, max_len: usize) -> String {
if kid.len() <= max_len {
kid.to_string()
} else {
kid.chars().enumerate()
.filter(|(i, _c)| *i < max_len)
.map(|(i, c)| iff!(i >= (max_len - 3), '.', c)).collect()
}
}
fn config_profiles(cmd_config: &CmdConfig, config: &TinyEncryptConfig) -> XResult<()> {
let mut reverse_map = HashMap::new();
if let Some(profiles) = &config.profiles {
for (p, v) in profiles {
let mut v2 = v.clone();
v2.sort();
let vs = v2.join(",");
match reverse_map.get_mut(&vs) {
None => { reverse_map.insert(vs, vec![(p, v)]); }
Some(vec) => { vec.push((p, v)); }
}
}
}
let mut config_profiles = vec![];
for pvs in reverse_map.values() {
let mut ps: Vec<_> = pvs.iter().map(|pv| pv.0).collect();
ps.sort();
let pp = ps.iter().map(|s| s.to_string()).collect::<Vec<_>>().join(", ");
if cmd_config.hide_all && pp == "__all__" {
continue;
}
let kids = pvs[0].1;
let mut ks = Vec::with_capacity(kids.len());
for kid in kids {
match config.find_by_kid(kid) {
None => {
ks.push(format!("[ERROR] Key not found: {}", kid));
}
Some(envelop) => {
let kid = if cmd_config.show_kid {
format!("Kid: {}", envelop.kid)
} else {
envelop.sid.as_ref()
.map(|sid| format!("Sid: {}", sid))
.unwrap_or_else(|| format!("Kid: {}", envelop.kid))
};
let desc = envelop.desc.as_ref()
.map(|desc| format!(", Desc: {}", desc))
.unwrap_or_else(|| "".to_string());
ks.push(format!(
"{}, {}{}",
util_envelop::with_width_type(envelop.r#type.get_name()), kid, desc
));
}
}
}
config_profiles.push(ConfigProfile {
profiles: pp,
keys: ks.join("\n"),
});
}
config_profiles.sort();
let mut table = Table::new(config_profiles);
table.with(Style::modern());
println!("{}", table);
Ok(())
}

File diff suppressed because it is too large Load Diff

125
src/cmd_directdecrypt.rs Normal file
View File

@@ -0,0 +1,125 @@
use std::fs;
use std::fs::File;
use std::io::{Read, Write};
use std::path::PathBuf;
use std::time::Instant;
use clap::Args;
use rust_util::{debugging, information, opt_result, simple_error, success, warning, XResult};
use zeroize::Zeroize;
use crate::{util, util_digest};
use crate::crypto_cryptor::{Cryptor, KeyNonce};
use crate::util_progress::Progress;
#[derive(Debug, Args)]
pub struct CmdDirectDecrypt {
/// File input
#[arg(long, short = 'i')]
pub file_in: PathBuf,
/// File output
#[arg(long, short = 'o')]
pub file_out: PathBuf,
/// Remove source file
#[arg(long, short = 'R')]
pub remove_file: bool,
/// Key in HEX (32 bytes)
#[arg(long, short = 'k')]
pub key: String,
}
impl Drop for CmdDirectDecrypt {
fn drop(&mut self) {
self.key.zeroize();
}
}
const DIRECT_ENCRYPT_MAGIC: &str = "e2c50001";
// Direct decrypt file format:
// [4 bytes] - magic 0xe2 0xc5 0x00 0x01
// [32 bytes] - key digest
// [12 bytes] - nonce
// [n bytes] - ciphertext
// [16 bytes] - tag
pub fn direct_decrypt(cmd_direct_decrypt: CmdDirectDecrypt) -> XResult<()> {
let key = opt_result!(hex::decode(&cmd_direct_decrypt.key), "Parse encryption key failed: {}");
if key.len() != 32 {
return simple_error!("Key length error, must be AES256.");
}
let mut file_in = opt_result!(File::open(&cmd_direct_decrypt.file_in), "Open in file failed: {}");
let file_in_len = file_in.metadata().map(|m| m.len()).unwrap_or(0);
if fs::metadata(&cmd_direct_decrypt.file_out).is_ok() {
return simple_error!("Out file exists.");
}
let mut magic = [0_u8; 4];
opt_result!(file_in.read_exact(&mut magic), "Read magic failed: {}");
if hex::encode(magic) != DIRECT_ENCRYPT_MAGIC {
return simple_error!("In file magic mismatch.");
}
let mut key_digest = [0_u8; 32];
opt_result!(file_in.read_exact(&mut key_digest), "Read encryption key digest failed: {}");
if hex::encode(util_digest::sha256_digest(&key)) != hex::encode(key_digest) {
debugging!("Encryption key digest mismatch: {} vs {}",
hex::encode(util_digest::sha256_digest(&key)), hex::encode(key_digest));
return simple_error!("Encryption key digest mismatch.");
}
let mut nonce = [0_u8; 12];
opt_result!(file_in.read_exact(&mut nonce), "Read nonce failed: {}");
let mut file_out = opt_result!(File::create(&cmd_direct_decrypt.file_out), "Create out file failed: {}");
let key_nonce = KeyNonce { k: &key, n: &nonce };
let instant = Instant::now();
let decrypted_len = opt_result!(
decrypt_file(&mut file_in, file_in_len, &mut file_out, Cryptor::Aes256Gcm, &key_nonce),
"Decrypt file {} -> {}, failed: {}",
cmd_direct_decrypt.file_in.display(),
cmd_direct_decrypt.file_out.display()
);
let elapsed_millis = instant.elapsed().as_millis();
success!("Decrypt file succeed: {}, file size: {} byte(s), elapsed: {} ms",
cmd_direct_decrypt.file_out.display(), decrypted_len, elapsed_millis);
util::zeroize(key);
nonce.zeroize();
drop(file_in);
drop(file_out);
if cmd_direct_decrypt.remove_file {
information!("Remove in file: {}", cmd_direct_decrypt.file_in.display());
if let Err(e) = fs::remove_file(&cmd_direct_decrypt.file_in) {
warning!("Remove in file failed: {}", e);
}
}
Ok(())
}
fn decrypt_file(file_in: &mut impl Read, file_len: u64, file_out: &mut impl Write,
cryptor: Cryptor, key_nonce: &KeyNonce) -> XResult<u64> {
let mut total_len = 0_u64;
let mut buffer = [0u8; 1024 * 8];
let progress = Progress::new(file_len);
let mut decryptor = cryptor.decryptor(key_nonce)?;
loop {
let len = opt_result!(file_in.read(&mut buffer), "Read in file failed: {}");
if len == 0 {
let last_block = opt_result!(decryptor.finalize(), "Decrypt in file failed: {}");
opt_result!(file_out.write_all(&last_block), "Write out file failed: {}");
progress.finish();
debugging!("Decrypt finished, total: {} byte(s)", total_len);
break;
} else {
total_len += len as u64;
let decrypted = decryptor.update(&buffer[0..len]);
opt_result!(file_out.write_all(&decrypted), "Write out file failed: {}");
progress.position(total_len);
}
}
Ok(total_len)
}

View File

@@ -7,51 +7,99 @@ use std::time::Instant;
use clap::Args; use clap::Args;
use flate2::Compression; use flate2::Compression;
use rsa::Pkcs1v15Encrypt; use rsa::Pkcs1v15Encrypt;
use rust_util::{debugging, failure, information, opt_result, simple_error, success, util_msg, warning, XResult}; use rust_util::util_time::UnixEpochTime;
use zeroize::Zeroize; use rust_util::{debugging, failure, iff, information, opt_result, simple_error, success, util_size, warning, XResult};
use crate::{util, util_ecdh, util_x25519};
use crate::compress::GzStreamEncoder; use crate::compress::GzStreamEncoder;
use crate::config::{TinyEncryptConfig, TinyEncryptConfigEnvelop}; use crate::config::{TinyEncryptConfig, TinyEncryptConfigEnvelop};
use crate::crypto_aes::aes_gcm_encrypt; use crate::consts::{ENC_AES256_GCM_KYBER1204, ENC_AES256_GCM_MLKEM1024, ENC_AES256_GCM_MLKEM768, ENC_AES256_GCM_P256, ENC_AES256_GCM_P384, ENC_AES256_GCM_X25519, ENC_CHACHA20_POLY1305_KYBER1204, ENC_CHACHA20_POLY1305_MLKEM1024, ENC_CHACHA20_POLY1305_MLKEM768, ENC_CHACHA20_POLY1305_P256, ENC_CHACHA20_POLY1305_P384, ENC_CHACHA20_POLY1305_X25519, SALT_COMMENT, TINY_ENC_FILE_EXT, TINY_ENC_PEM_FILE_EXT, TINY_ENC_PEM_NAME};
use crate::crypto_rsa::parse_spki; use crate::crypto_cryptor::{Cryptor, KeyNonce};
use crate::spec::{EncMetadata, TINY_ENCRYPT_VERSION_10, TinyEncryptEnvelop, TinyEncryptEnvelopType, TinyEncryptMeta}; use crate::spec::{
use crate::util::{ENC_AES256_GCM_P256, ENC_AES256_GCM_X25519, TINY_ENC_CONFIG_FILE}; EncEncryptedMeta, EncMetadata,
TinyEncryptEnvelop, TinyEncryptEnvelopType, TinyEncryptMeta,
};
use crate::util::{decode_base64, is_tiny_enc_file, to_pem};
use crate::util_ecdh::{ecdh_kyber1024, ecdh_p256, ecdh_p384, ecdh_x25519};
use crate::util_progress::Progress;
use crate::{util_mlkem, util_rsa};
use crate::wrap_key::{WrapKey, WrapKeyHeader}; use crate::wrap_key::{WrapKey, WrapKeyHeader};
use crate::{crypto_cryptor, crypto_simple, util, util_enc_file, util_env, util_gpg};
use crate::temporary_key::parse_temporary_keys;
use crate::util_mlkem::MlKemAlgo;
#[derive(Debug, Args)] #[derive(Debug, Args)]
pub struct CmdEncrypt { pub struct CmdEncrypt {
/// Files need to be decrypted /// Plaintext comment
pub paths: Vec<PathBuf>,
/// Comment
#[arg(long, short = 'c')] #[arg(long, short = 'c')]
pub comment: Option<String>, pub comment: Option<String>,
/// Encrypted comment /// Encrypted comment
#[arg(long, short = 'C')] #[arg(long, short = 'C')]
pub encrypted_comment: Option<String>, pub encrypted_comment: Option<String>,
/// Encryption profile
/// Encryption profile (use default when --key-filter is assigned)
#[arg(long, short = 'p')] #[arg(long, short = 'p')]
pub profile: Option<String>, pub profile: Option<String>,
/// Encryption key filter (key_id or type:TYPE(e.g. ecdh, pgp, ecdh-p384, pgp-ed25519), multiple joined by ',', ALL for all)
#[arg(long, short = 'k')]
pub key_filter: Option<String>,
/// Temporary key
#[arg(long)]
pub temporary_key: Option<Vec<String>>,
/// Compress before encrypt /// Compress before encrypt
#[arg(long, short = 'x')] #[arg(long, short = 'x')]
pub compress: bool, pub compress: bool,
/// Compress level (from 0[none], 1[fast] .. 6[default] .. to 9[best]) /// Compress level (from 0[none], 1[fast] .. 6[default] .. to 9[best])
#[arg(long, short = 'L')] #[arg(long, short = 'L')]
pub compress_level: Option<u32>, pub compress_level: Option<u32>,
/// Compatible with 1.0
#[arg(long, short = '1')]
pub compatible_with_1_0: bool,
/// Remove source file /// Remove source file
#[arg(long, short = 'R')] #[arg(long, short = 'R')]
pub remove_file: bool, pub remove_file: bool,
/// Create file (create a empty encrypted file)
#[arg(long, short = 'a')]
pub create: bool,
/// Disable compress meta
#[arg(long)]
pub disable_compress_meta: bool,
/// Output file in PEM format (alias --pem)
#[arg(long, alias = "pem")]
pub pem_output: bool,
/// Encryption algorithm (AES/GCM, CHACHA20/POLY1305 or AES, CHACHA20, default AES/GCM)
#[arg(long, short = 'A')]
pub encryption_algorithm: Option<String>,
/// Config file or based64 encoded (starts with: base64:)
#[arg(long)]
pub config: Option<String>,
/// Files need to be decrypted
pub paths: Vec<PathBuf>,
} }
pub fn encrypt(cmd_encrypt: CmdEncrypt) -> XResult<()> { pub fn encrypt(cmd_encrypt: CmdEncrypt) -> XResult<()> {
let config = TinyEncryptConfig::load(TINY_ENC_CONFIG_FILE)?; let config = TinyEncryptConfig::load_default(&cmd_encrypt.config)?;
debugging!("Found tiny encrypt config: {:?}", config); debugging!("Found tiny encrypt config: {:?}", config);
let envelops = config.find_envelops(&cmd_encrypt.profile)?; let mut envelops = config.find_envelops(&cmd_encrypt.profile, &cmd_encrypt.key_filter)?;
if envelops.is_empty() { return simple_error!("Cannot find any valid envelops"); }
debugging!("Found envelops: {:?}", envelops); debugging!("Found envelops: {:?}", envelops);
let temporary_envelops = parse_temporary_keys(&cmd_encrypt.temporary_key)?;
if !temporary_envelops.is_empty() {
for t_envelop in &temporary_envelops {
envelops.push(t_envelop)
}
debugging!("Final envelops: {:?}", envelops);
}
if envelops.is_empty() { return simple_error!("Cannot find any valid envelops"); }
let envelop_tkids: Vec<_> = envelops.iter() let envelop_tkids: Vec<_> = envelops.iter()
.map(|e| format!("{}:{}", e.r#type.get_name(), e.kid)) .map(|e| format!("{}:{}", e.r#type.get_name(), e.kid))
.collect(); .collect();
@@ -64,8 +112,9 @@ pub fn encrypt(cmd_encrypt: CmdEncrypt) -> XResult<()> {
let mut failed_count = 0; let mut failed_count = 0;
let mut total_len = 0_u64; let mut total_len = 0_u64;
for path in &cmd_encrypt.paths { for path in &cmd_encrypt.paths {
let path = config.resolve_path_namespace(path, false);
let start_encrypt_single = Instant::now(); let start_encrypt_single = Instant::now();
match encrypt_single(path, &envelops, &cmd_encrypt) { match encrypt_single(&path, &envelops, &cmd_encrypt) {
Ok(len) => { Ok(len) => {
total_len += len; total_len += len;
if len > 0 { succeed_count += 1; } else { skipped_count += 1; } if len > 0 { succeed_count += 1; } else { skipped_count += 1; }
@@ -95,149 +144,201 @@ pub fn encrypt(cmd_encrypt: CmdEncrypt) -> XResult<()> {
Ok(()) Ok(())
} }
fn encrypt_single(path: &PathBuf, envelops: &[&TinyEncryptConfigEnvelop], cmd_encrypt: &CmdEncrypt) -> XResult<u64> { pub fn encrypt_single(path: &PathBuf, envelops: &[&TinyEncryptConfigEnvelop], cmd_encrypt: &CmdEncrypt) -> XResult<u64> {
let path_display = format!("{}", path.display()); let path_display = format!("{}", path.display());
if path_display.ends_with(util::TINY_ENC_FILE_EXT) { let path_out = if cmd_encrypt.pem_output {
format!("{}{}", path_display, TINY_ENC_PEM_FILE_EXT)
} else {
format!("{}{}", path_display, TINY_ENC_FILE_EXT)
};
let encrypt_single_result = encrypt_single_file_out(path, &path_out, envelops, cmd_encrypt);
if cmd_encrypt.create {
if let Ok(content) = fs::read_to_string(path) {
if content == "\n" {
let _ = fs::remove_file(path);
}
}
}
encrypt_single_result
}
pub fn encrypt_single_file_out(path: &PathBuf, path_out: &str, envelops: &[&TinyEncryptConfigEnvelop], cmd_encrypt: &CmdEncrypt) -> XResult<u64> {
let path_display = format!("{}", path.display());
if is_tiny_enc_file(&path_display) {
information!("Tiny enc file skipped: {}", path_display); information!("Tiny enc file skipped: {}", path_display);
return Ok(0); return Ok(0);
} }
util::require_file_exists(path)?; let cryptor = crypto_cryptor::get_cryptor_by_encryption_algorithm(&cmd_encrypt.encryption_algorithm)?;
information!("Using encryption algorithm: {}", cryptor.get_name());
if cmd_encrypt.create {
util::require_file_not_exists(path)?;
opt_result!(fs::write(path, "\n"), "Write empty file failed: {}");
} else {
util::require_file_exists(path)?;
}
let mut file_in = opt_result!(File::open(path), "Open file: {} failed: {}", &path_display); let mut file_in = opt_result!(File::open(path), "Open file: {} failed: {}", &path_display);
let path_out = format!("{}{}", path_display, util::TINY_ENC_FILE_EXT); util::require_file_not_exists(path_out)?;
util::require_file_not_exists(path_out.as_str())?;
let (key, nonce) = util::make_key256_and_nonce(); let (key, nonce) = util::make_key256_and_nonce();
let envelops = encrypt_envelops(&key, &envelops)?; let key_nonce = KeyNonce { k: key.as_ref(), n: nonce.as_ref() };
// Encrypt session key to envelops
let envelops = encrypt_envelops(cryptor, key.as_ref(), envelops)?;
let encrypted_comment = match &cmd_encrypt.encrypted_comment { let encrypted_comment = match &cmd_encrypt.encrypted_comment {
None => None, None => None,
Some(encrypted_comment) => Some(util::encode_base64( Some(encrypted_comment) => Some(util::encode_base64(
&aes_gcm_encrypt(&key, &nonce, encrypted_comment.as_bytes())?)) &crypto_simple::encrypt_with_salt(
cryptor, &key_nonce, SALT_COMMENT, encrypted_comment.as_bytes())?))
}; };
let file_metadata = opt_result!(fs::metadata(path), "Read file: {} meta failed: {}", path.display()); let file_metadata = opt_result!(fs::metadata(path), "Read file: {} meta failed: {}", path.display());
let enc_encrypted_meta = EncEncryptedMeta {
filename: Some(util::get_file_name(path)),
c_time: file_metadata.created().ok().and_then(|t| t.to_millis()),
m_time: file_metadata.modified().ok().and_then(|t| t.to_millis()),
};
let enc_encrypted_meta_bytes = opt_result!(enc_encrypted_meta.seal(
cryptor, &key_nonce), "Seal enc-encrypted-meta failed: {}");
let compress_level = get_compress_level(cmd_encrypt, &path_display, cmd_encrypt.pem_output);
debugging!("Compress level: {:?}", compress_level);
let enc_metadata = EncMetadata { let enc_metadata = EncMetadata {
comment: cmd_encrypt.comment.clone(), comment: cmd_encrypt.comment.clone(),
encrypted_comment, encrypted_comment,
encrypted_meta: None, encrypted_meta: Some(util::encode_base64(&enc_encrypted_meta_bytes)),
compress: cmd_encrypt.compress, compress: compress_level.is_some(),
}; };
let mut encrypt_meta = TinyEncryptMeta::new(&file_metadata, &enc_metadata, &nonce, envelops); let encrypt_meta = TinyEncryptMeta::new(
&file_metadata, &enc_metadata, cryptor, nonce.as_ref(), envelops);
debugging!("Encrypted meta: {:?}", encrypt_meta); debugging!("Encrypted meta: {:?}", encrypt_meta);
if cmd_encrypt.compatible_with_1_0 {
if let Some(envelops) = encrypt_meta.envelops {
let mut filter_envelops = vec![];
for envelop in envelops {
if (envelop.r#type == TinyEncryptEnvelopType::Pgp) && encrypt_meta.pgp_envelop.is_none() {
encrypt_meta.pgp_fingerprint = Some(format!("KID:{}", envelop.kid));
encrypt_meta.pgp_envelop = Some(envelop.encrypted_key.clone());
} else if (envelop.r#type == TinyEncryptEnvelopType::Ecdh) && encrypt_meta.ecdh_envelop.is_none() {
encrypt_meta.ecdh_point = Some(format!("KID:{}", envelop.kid));
encrypt_meta.ecdh_envelop = Some(envelop.encrypted_key.clone());
} else {
filter_envelops.push(envelop);
}
}
encrypt_meta.envelops = if filter_envelops.is_empty() { None } else { Some(filter_envelops) };
if encrypt_meta.envelops.is_none() {
encrypt_meta.version = TINY_ENCRYPT_VERSION_10.to_string();
}
}
}
let mut file_out = File::create(&path_out)?;
opt_result!(file_out.write_all(&util::TINY_ENC_MAGIC_TAG.to_be_bytes()), "Write tag failed: {}");
let encrypted_meta_bytes = opt_result!(serde_json::to_vec(&encrypt_meta), "Generate meta json bytes failed: {}");
let encrypted_meta_bytes_len = encrypted_meta_bytes.len() as u32;
opt_result!(file_out.write_all(&encrypted_meta_bytes_len.to_be_bytes()), "Write meta len failed: {}");
opt_result!(file_out.write_all(&encrypted_meta_bytes), "Write meta failed: {}");
let start = Instant::now(); let start = Instant::now();
util_msg::print_lastline(&format!("Encrypting file: {} ...", path_display));
encrypt_file(&mut file_in, &mut file_out, &key, &nonce, cmd_encrypt.compress, &cmd_encrypt.compress_level)?;
util_msg::clear_lastline();
let encrypt_duration = start.elapsed();
debugging!("Encrypt file: {} elapsed: {} ms", path_display, encrypt_duration.as_millis());
util::zeroize(key); let mut file_out = File::create(path_out)?;
util::zeroize(nonce); let compress_meta = !cmd_encrypt.disable_compress_meta;
drop(file_in);
drop(file_out); if cmd_encrypt.pem_output {
if cmd_encrypt.remove_file { let temp_output_len = file_in.metadata().map(|m| m.len()).unwrap_or(0) + 1024 * 8;
match fs::remove_file(path) { if temp_output_len > 8 * 1024 * 1028 {
Err(e) => warning!("Remove file: {} failed: {}", path_display, e), warning!("Input file is more than 8 MiB.");
Ok(_) => information!("Remove file: {} succeed", path_display),
} }
if temp_output_len > 32 * 1024 * 1028 {
return simple_error!("Input file is too large, file is {} bytes", temp_output_len);
}
let mut temp_output = Vec::with_capacity(temp_output_len as usize);
let _ = util_enc_file::write_tiny_encrypt_meta(&mut temp_output, &encrypt_meta, compress_meta)?;
encrypt_file(&mut file_in, file_metadata.len(), &mut temp_output,
cryptor, &key_nonce, &compress_level,
)?;
let temp_output_pem = to_pem(&temp_output, TINY_ENC_PEM_NAME);
file_out.write_all(temp_output_pem.as_bytes())?;
} else {
let _ = util_enc_file::write_tiny_encrypt_meta(&mut file_out, &encrypt_meta, compress_meta)?;
encrypt_file(&mut file_in, file_metadata.len(), &mut file_out,
cryptor, &key_nonce, &compress_level,
)?;
} }
drop(file_out);
let encrypt_duration = start.elapsed();
let compress_desc = iff!(compress_level.is_some(), " [with compress]", "");
debugging!("Inner encrypt file{}: {} elapsed: {} ms", compress_desc, path_display, encrypt_duration.as_millis());
if cmd_encrypt.remove_file { util::remove_file_with_msg(path); }
Ok(file_metadata.len()) Ok(file_metadata.len())
} }
pub(crate) fn encrypt_file(file_in: &mut impl Read, file_len: u64, file_out: &mut impl Write, cryptor: Cryptor,
fn encrypt_file(file_in: &mut File, file_out: &mut File, key: &[u8], nonce: &[u8], compress: bool, compress_level: &Option<u32>) -> XResult<usize> { key_nonce: &KeyNonce, compress_level: &Option<u32>) -> XResult<u64> {
let mut total_len = 0; let compress = compress_level.is_some();
let mut total_len = 0_u64;
let mut write_len = 0_u64;
let mut buffer = [0u8; 1024 * 8]; let mut buffer = [0u8; 1024 * 8];
let key = opt_result!(key.try_into(), "Key is not 32 bytes: {}");
let mut gz_encoder = match compress_level { let mut gz_encoder = match compress_level {
None => GzStreamEncoder::new_default(), None => GzStreamEncoder::new_default(),
Some(compress_level) => { Some(compress_level) => {
if *compress_level > 9 { if *compress_level > 9 {
return simple_error!("Compress level must in range [0, 9]"); return simple_error!("Compress level must be in range [0, 9]");
} }
GzStreamEncoder::new(Compression::new(*compress_level)) GzStreamEncoder::new(Compression::new(*compress_level))
} }
}; };
let mut encryptor = aes_gcm_stream::Aes256GcmStreamEncryptor::new(key, &nonce); let progress = Progress::new(file_len);
let mut encryptor = cryptor.encryptor(key_nonce)?;
loop { loop {
let len = opt_result!(file_in.read(&mut buffer), "Read file failed: {}"); let len = opt_result!(file_in.read(&mut buffer), "Read file failed: {}");
if len == 0 { if len == 0 {
let last_block = if compress { let last_block_and_tag = if compress {
let last_compressed_buffer = opt_result!(gz_encoder.finalize(), "Decompress file failed: {}"); let last_compressed_buffer = opt_result!(gz_encoder.finalize(), "Decompress file failed: {}");
let mut encrypted_block = encryptor.update(&last_compressed_buffer); let mut encrypted_block = encryptor.update(&last_compressed_buffer);
let (last_block, tag) = encryptor.finalize(); let (last_block, tag) = encryptor.finalize();
write_len += encrypted_block.len() as u64;
write_len += last_block.len() as u64;
encrypted_block.extend_from_slice(&last_block); encrypted_block.extend_from_slice(&last_block);
encrypted_block.extend_from_slice(&tag); encrypted_block.extend_from_slice(&tag);
encrypted_block encrypted_block
} else { } else {
let (mut last_block, tag) = encryptor.finalize(); let (mut last_block, tag) = encryptor.finalize();
write_len += last_block.len() as u64;
last_block.extend_from_slice(&tag); last_block.extend_from_slice(&tag);
last_block last_block
}; };
opt_result!(file_out.write_all(&last_block), "Write file failed: {}"); opt_result!(file_out.write_all(&last_block_and_tag), "Write file failed: {}");
debugging!("Encrypt finished, total bytes: {}", total_len); progress.finish();
debugging!("Encrypt finished, total bytes: {} byte(s)", total_len);
if compress {
information!("File is compressed: {} -> {}, ratio: {}%",
util_size::get_display_size(total_len as i64),
util_size::get_display_size(write_len as i64),
util::ratio(write_len, total_len));
}
break; break;
} else { } else {
total_len += len; total_len += len as u64;
let encrypted = if compress { let encrypted = if compress {
let compressed = opt_result!(gz_encoder.update(&buffer[0..len]), "Decompress file failed: {}"); let compressed = opt_result!(gz_encoder.update(&buffer[0..len]), "Decompress file failed: {}");
encryptor.update(&compressed) encryptor.update(&compressed)
} else { } else {
encryptor.update(&buffer[0..len]) encryptor.update(&buffer[0..len])
}; };
write_len += encrypted.len() as u64;
opt_result!(file_out.write_all(&encrypted), "Write file failed: {}"); opt_result!(file_out.write_all(&encrypted), "Write file failed: {}");
progress.position(total_len);
} }
} }
let mut key = key;
key.zeroize();
Ok(total_len) Ok(total_len)
} }
fn encrypt_envelops(key: &[u8], envelops: &[&TinyEncryptConfigEnvelop]) -> XResult<Vec<TinyEncryptEnvelop>> { pub fn encrypt_envelops(cryptor: Cryptor, key: &[u8], envelops: &[&TinyEncryptConfigEnvelop]) -> XResult<Vec<TinyEncryptEnvelop>> {
let mut encrypted_envelops = vec![]; let mut encrypted_envelops = vec![];
for envelop in envelops { for envelop in envelops {
match envelop.r#type { match envelop.r#type {
TinyEncryptEnvelopType::Pgp => { TinyEncryptEnvelopType::PgpRsa | TinyEncryptEnvelopType::PivRsa => {
encrypted_envelops.push(encrypt_envelop_pgp(key, envelop)?); encrypted_envelops.push(encrypt_envelop_rsa(key, envelop)?);
} }
TinyEncryptEnvelopType::PgpX25519 => { TinyEncryptEnvelopType::Gpg => {
encrypted_envelops.push(encrypt_envelop_ecdh_x25519(key, envelop)?); encrypted_envelops.push(encrypt_envelop_gpg(key, envelop)?);
} }
TinyEncryptEnvelopType::Ecdh => { TinyEncryptEnvelopType::PgpX25519 | TinyEncryptEnvelopType::StaticX25519 => {
encrypted_envelops.push(encrypt_envelop_ecdh(key, envelop)?); encrypted_envelops.push(encrypt_envelop_ecdh_x25519(cryptor, key, envelop)?);
}
TinyEncryptEnvelopType::PivP256 | TinyEncryptEnvelopType::KeyP256 | TinyEncryptEnvelopType::ExtP256 => {
encrypted_envelops.push(encrypt_envelop_ecdh_p256(cryptor, key, envelop)?);
}
TinyEncryptEnvelopType::PivP384 | TinyEncryptEnvelopType::ExtP384 => {
encrypted_envelops.push(encrypt_envelop_ecdh_p384(cryptor, key, envelop)?);
}
TinyEncryptEnvelopType::StaticKyber1024 => {
encrypted_envelops.push(encrypt_envelop_ecdh_kyber1204(cryptor, key, envelop)?);
}
TinyEncryptEnvelopType::KeyMlKem768 | TinyEncryptEnvelopType::KeyMlKem1024 |
TinyEncryptEnvelopType::ExtMlKem768 | TinyEncryptEnvelopType::ExtMlKem1024 => {
encrypted_envelops.push(encrypt_envelop_ecdh_ml_kem(cryptor, key, envelop)?);
} }
_ => return simple_error!("Not supported type: {:?}", envelop.r#type), _ => return simple_error!("Not supported type: {:?}", envelop.r#type),
} }
@@ -245,37 +346,75 @@ fn encrypt_envelops(key: &[u8], envelops: &[&TinyEncryptConfigEnvelop]) -> XResu
Ok(encrypted_envelops) Ok(encrypted_envelops)
} }
fn encrypt_envelop_ecdh(key: &[u8], envelop: &TinyEncryptConfigEnvelop) -> XResult<TinyEncryptEnvelop> { fn encrypt_envelop_ecdh_p256(cryptor: Cryptor, key: &[u8], envelop: &TinyEncryptConfigEnvelop) -> XResult<TinyEncryptEnvelop> {
let public_key_point_hex = &envelop.public_part; let public_key_point_hex = &envelop.public_part;
let (shared_secret, ephemeral_spki) = util_ecdh::compute_shared_secret(public_key_point_hex)?; let (shared_secret, ephemeral_spki) = ecdh_p256::compute_p256_shared_secret(public_key_point_hex)?;
let enc_type = match cryptor {
encrypt_envelop_shared_secret(key, &shared_secret, &ephemeral_spki, ENC_AES256_GCM_P256, envelop) Cryptor::Aes256Gcm => ENC_AES256_GCM_P256,
Cryptor::ChaCha20Poly1305 => ENC_CHACHA20_POLY1305_P256,
};
encrypt_envelop_shared_secret(cryptor, key, &shared_secret, &ephemeral_spki, enc_type, envelop)
} }
fn encrypt_envelop_ecdh_x25519(key: &[u8], envelop: &TinyEncryptConfigEnvelop) -> XResult<TinyEncryptEnvelop> { fn encrypt_envelop_ecdh_p384(cryptor: Cryptor, key: &[u8], envelop: &TinyEncryptConfigEnvelop) -> XResult<TinyEncryptEnvelop> {
let public_key_point_hex = &envelop.public_part; let public_key_point_hex = &envelop.public_part;
let (shared_secret, ephemeral_spki) = util_x25519::compute_x25519_shared_secret(public_key_point_hex)?; let (shared_secret, ephemeral_spki) = ecdh_p384::compute_p384_shared_secret(public_key_point_hex)?;
let enc_type = match cryptor {
encrypt_envelop_shared_secret(key, &shared_secret, &ephemeral_spki, ENC_AES256_GCM_X25519, envelop) Cryptor::Aes256Gcm => ENC_AES256_GCM_P384,
Cryptor::ChaCha20Poly1305 => ENC_CHACHA20_POLY1305_P384,
};
encrypt_envelop_shared_secret(cryptor, key, &shared_secret, &ephemeral_spki, enc_type, envelop)
} }
fn encrypt_envelop_shared_secret(key: &[u8], fn encrypt_envelop_ecdh_x25519(cryptor: Cryptor, key: &[u8], envelop: &TinyEncryptConfigEnvelop) -> XResult<TinyEncryptEnvelop> {
let public_key_point_hex = &envelop.public_part;
let (shared_secret, ephemeral_spki) = ecdh_x25519::compute_x25519_shared_secret(public_key_point_hex)?;
let enc_type = match cryptor {
Cryptor::Aes256Gcm => ENC_AES256_GCM_X25519,
Cryptor::ChaCha20Poly1305 => ENC_CHACHA20_POLY1305_X25519,
};
encrypt_envelop_shared_secret(cryptor, key, &shared_secret, &ephemeral_spki, enc_type, envelop)
}
fn encrypt_envelop_ecdh_kyber1204(cryptor: Cryptor, key: &[u8], envelop: &TinyEncryptConfigEnvelop) -> XResult<TinyEncryptEnvelop> {
let public_key_point_hex = &envelop.public_part;
let (shared_secret, ephemeral_spki) = ecdh_kyber1024::compute_kyber1024_shared_secret(public_key_point_hex)?;
let enc_type = match cryptor {
Cryptor::Aes256Gcm => ENC_AES256_GCM_KYBER1204,
Cryptor::ChaCha20Poly1305 => ENC_CHACHA20_POLY1305_KYBER1204,
};
encrypt_envelop_shared_secret(cryptor, key, &shared_secret, &ephemeral_spki, enc_type, envelop)
}
fn encrypt_envelop_ecdh_ml_kem(cryptor: Cryptor, key: &[u8], envelop: &TinyEncryptConfigEnvelop) -> XResult<TinyEncryptEnvelop> {
let public_key_base64 = &envelop.public_part;
let public_key = opt_result!(decode_base64(public_key_base64), "Decode ML-KEM public key from base64 failed: {}");
let (shared_secret, ciphertext, ml_kem_algo) = util_mlkem::try_ml_kem_encapsulate(&public_key)?;
let enc_type = match (cryptor, ml_kem_algo) {
(Cryptor::Aes256Gcm, MlKemAlgo::MlKem768) => ENC_AES256_GCM_MLKEM768,
(Cryptor::Aes256Gcm, MlKemAlgo::MlKem1024) => ENC_AES256_GCM_MLKEM1024,
(Cryptor::ChaCha20Poly1305, MlKemAlgo::MlKem768) => ENC_CHACHA20_POLY1305_MLKEM768,
(Cryptor::ChaCha20Poly1305, MlKemAlgo::MlKem1024) => ENC_CHACHA20_POLY1305_MLKEM1024,
};
encrypt_envelop_shared_secret(cryptor, key, &shared_secret, &ciphertext, enc_type, envelop)
}
fn encrypt_envelop_shared_secret(cryptor: Cryptor,
key: &[u8],
shared_secret: &[u8], shared_secret: &[u8],
ephemeral_spki: &[u8], ephemeral_spki: &[u8],
enc_type: &str, enc_type: &str,
envelop: &TinyEncryptConfigEnvelop) -> XResult<TinyEncryptEnvelop> { envelop: &TinyEncryptConfigEnvelop) -> XResult<TinyEncryptEnvelop> {
let shared_key = util::simple_kdf(shared_secret); let shared_key = util::simple_kdf(shared_secret);
let (_, nonce) = util::make_key256_and_nonce(); let nonce = util::make_nonce();
let key_nonce = KeyNonce { k: &shared_key, n: nonce.as_ref() };
let encrypted_key = aes_gcm_encrypt(&shared_key, &nonce, key)?; let encrypted_key = crypto_simple::encrypt(
cryptor, &key_nonce, key)?;
let wrap_key = WrapKey { let wrap_key = WrapKey {
header: WrapKeyHeader { header: WrapKeyHeader::from(enc_type, ephemeral_spki),
kid: Some(envelop.kid.clone()), nonce: nonce.0.clone(),
enc: enc_type.to_string(),
e_pub_key: util::encode_base64_url_no_pad(&ephemeral_spki),
},
nonce,
encrypted_data: encrypted_key, encrypted_data: encrypted_key,
}; };
let encoded_wrap_key = wrap_key.encode()?; let encoded_wrap_key = wrap_key.encode()?;
@@ -283,19 +422,47 @@ fn encrypt_envelop_shared_secret(key: &[u8],
Ok(TinyEncryptEnvelop { Ok(TinyEncryptEnvelop {
r#type: envelop.r#type, r#type: envelop.r#type,
kid: envelop.kid.clone(), kid: envelop.kid.clone(),
desc: envelop.desc.clone(), desc: None,
encrypted_key: encoded_wrap_key, encrypted_key: encoded_wrap_key,
}) })
} }
fn encrypt_envelop_pgp(key: &[u8], envelop: &TinyEncryptConfigEnvelop) -> XResult<TinyEncryptEnvelop> { fn encrypt_envelop_rsa(key: &[u8], envelop: &TinyEncryptConfigEnvelop) -> XResult<TinyEncryptEnvelop> {
let pgp_public_key = opt_result!(parse_spki(&envelop.public_part), "Parse PGP public key failed: {}"); let rsa_public_key = opt_result!(util_rsa::parse_spki(&envelop.public_part), "Parse RSA public key failed: {}");
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
let encrypted_key = opt_result!(pgp_public_key.encrypt(&mut rng, Pkcs1v15Encrypt, key), "PGP public key encrypt failed: {}"); let encrypted_key = opt_result!(rsa_public_key.encrypt(&mut rng, Pkcs1v15Encrypt, key), "RSA public key encrypt failed: {}");
Ok(TinyEncryptEnvelop { Ok(TinyEncryptEnvelop {
r#type: envelop.r#type, r#type: envelop.r#type,
kid: envelop.kid.clone(), kid: envelop.kid.clone(),
desc: envelop.desc.clone(), desc: None,
encrypted_key: util::encode_base64(&encrypted_key), encrypted_key: util::encode_base64(&encrypted_key),
}) })
} }
fn encrypt_envelop_gpg(key: &[u8], envelop: &TinyEncryptConfigEnvelop) -> XResult<TinyEncryptEnvelop> {
let encrypted_key = opt_result!(util_gpg::gpg_encrypt(&envelop.public_part, key), "GPG encrypt failed: {}");
Ok(TinyEncryptEnvelop {
r#type: envelop.r#type,
kid: envelop.kid.clone(),
desc: None,
encrypted_key,
})
}
fn get_compress_level(cmd_encrypt: &CmdEncrypt, path: &str, pem_output: bool) -> Option<u32> {
let mut auto_compress = false;
let path_parts = path.split(".").collect::<Vec<_>>();
let path_ext = path_parts[path_parts.len() - 1].to_lowercase();
if let Some(auto_compress_file_exts) = util_env::get_auto_compress_file_exts() {
auto_compress = auto_compress_file_exts.contains(&path_ext);
debugging!("File ext: {} matches auto compress exts: {:?}", path_ext, auto_compress_file_exts);
}
if auto_compress || cmd_encrypt.compress || util_env::get_default_compress().unwrap_or(false) {
Some(cmd_encrypt.compress_level.unwrap_or_else(|| Compression::default().level()))
} else if pem_output {
Some(Compression::best().level())
} else {
None
}
}

165
src/cmd_execenv.rs Normal file
View File

@@ -0,0 +1,165 @@
use std::fs::File;
use std::path::PathBuf;
use std::process::Command;
use std::time::Instant;
use clap::Args;
use rust_util::{debugging, iff, opt_result, simple_error, util_cmd, util_msg, warning, XResult};
use serde_json::Value;
use zeroize::Zeroize;
use crate::{config, consts, util, util_env};
use crate::cmd_decrypt::{decrypt_limited_content_to_vec, select_envelop, try_decrypt_key};
use crate::config::TinyEncryptConfig;
use crate::crypto_cryptor::{Cryptor, KeyNonce};
use crate::util::SecVec;
use crate::util_enc_file;
#[derive(Debug, Args)]
pub struct CmdExecEnv {
/// PGP or PIV PIN
#[arg(long, short = 'p')]
pub pin: Option<String>,
/// KeyID
#[arg(long, short = 'k')]
pub key_id: Option<String>,
/// PIV slot
#[arg(long, short = 's')]
pub slot: Option<String>,
/// Config file or based64 encoded (starts with: base64:)
#[arg(long)]
pub config: Option<String>,
/// Tiny encrypt file name
pub file_name: String,
/// Command and arguments
pub command_arguments: Vec<String>,
}
impl Drop for CmdExecEnv {
fn drop(&mut self) {
if let Some(p) = self.pin.as_mut() { p.zeroize(); }
}
}
pub fn exec_env(cmd_exec_env: CmdExecEnv) -> XResult<()> {
util_msg::set_logger_std_out(false);
debugging!("Cmd exec env: {:?}", cmd_exec_env);
let config = TinyEncryptConfig::load_default(&cmd_exec_env.config).ok();
if cmd_exec_env.command_arguments.is_empty() {
return simple_error!("No commands assigned.");
}
let start = Instant::now();
let pin = cmd_exec_env.pin.clone().or_else(util_env::get_pin);
let key_id = cmd_exec_env.key_id.clone().or_else(util_env::get_key_id);
let path = PathBuf::from(&cmd_exec_env.file_name);
let path = config::resolve_path_namespace(&config, &path, true);
let path_display = format!("{}", &path.display());
util::require_tiny_enc_file_and_exists(&path)?;
let mut file_in = opt_result!(File::open(&path), "Open file: {} failed: {}", &path_display);
let (_, _, meta) = opt_result!(
util_enc_file::read_tiny_encrypt_meta_and_normalize(&mut file_in), "Read file: {}, failed: {}", &path_display);
util_msg::when_debug(|| {
debugging!("Found meta: {}", serde_json::to_string_pretty(&meta).unwrap());
});
let encryption_algorithm = meta.encryption_algorithm.as_deref()
.unwrap_or(consts::TINY_ENC_AES_GCM);
let cryptor = Cryptor::from(encryption_algorithm)?;
let selected_envelop = select_envelop(&meta, &key_id, &config, true)?;
let key = SecVec(try_decrypt_key(&config, selected_envelop, &pin, &cmd_exec_env.slot, true)?);
let nonce = SecVec(opt_result!(util::decode_base64(&meta.nonce), "Decode nonce failed: {}"));
let key_nonce = KeyNonce { k: key.as_ref(), n: nonce.as_ref() };
let decrypted_content = decrypt_limited_content_to_vec(&mut file_in, &meta, cryptor, &key_nonce)?;
let exit_code = if let Some(output) = decrypted_content {
debugging!("Outputs: {}", output);
let arguments = &cmd_exec_env.command_arguments;
let envs = parse_output_to_env(&output);
let mut command = Command::new(&arguments[0]);
arguments.iter().skip(1).for_each(|a| { command.arg(a); });
envs.iter().for_each(|(k, v)| { command.env(k, v); });
debugging!("Run cmd: {:?}", command);
let run_cmd_result = util_cmd::run_command_and_wait(&mut command)?;
debugging!("Run cmd result: {}", run_cmd_result);
iff!(run_cmd_result.success(), 0, run_cmd_result.code().unwrap_or(-2))
} else {
-1
};
debugging!("Finished, cost: {}ms", start.elapsed().as_millis());
std::process::exit(exit_code);
}
// supports format:
// JSON:
// {
// "KEY": "value",
// "KEY2": "value2"
// }
// ----OR----
// [
// "KEY": "value",
// "KEY2": "value2"
// ]
// ENV:
// KEY=value
// KEY2=value2
fn parse_output_to_env(output: &str) -> Vec<(String, String)> {
let mut env = vec![];
if let Ok(json) = serde_json::from_str::<Value>(output) {
match &json {
Value::Array(array) => {
for a in array {
match a {
Value::String(s) => { env.push((s.to_string(), "".to_string())); }
Value::Array(a2) => if a2.len() == 2 {
env.push((a2[0].to_string(), a2[1].to_string()));
} else {
warning!("Invalid array object: {:?}", a2);
}
Value::Object(object) => {
object.iter().for_each(|(k, v)| {
env.push((k.to_string(), v.to_string()));
});
}
_ => { warning!("Invalid array object: {}", a); }
}
}
}
Value::Object(object) => {
object.iter().for_each(|(k, v)| {
env.push((k.to_string(), v.to_string()));
});
}
_ => { warning!("Parse to env failed: {}", json); }
}
} else {
let lines = output.split('\n');
lines.filter(|ln| !ln.trim().is_empty()).for_each(|ln| {
if ln.starts_with('#') {
debugging!("Found comment: {}", ln);
} else if ln.contains('=') {
let k = ln.chars().take_while(|c| c != &'=').collect::<String>();
let v = ln.chars().skip_while(|c| c != &'=').skip(1).collect::<String>();
env.push((k, v));
} else {
env.push((ln.to_string(), "".to_string()));
}
});
}
debugging!("Parsed env: {:?}", env);
env
}

View File

@@ -1,48 +1,64 @@
use std::cmp::max; use std::cmp::max;
use std::fs::File; use std::fs::File;
use std::ops::Add;
use std::path::PathBuf; use std::path::PathBuf;
use std::time::{Duration, SystemTime}; use std::time::{Duration, SystemTime};
use clap::Args; use clap::Args;
use rust_util::{iff, opt_result, simple_error, success, util_time, warning, XResult}; use rust_util::{
debugging, failure, iff, opt_result, simple_error, success,
util_msg, util_size, util_time, XResult,
};
use rust_util::util_time::UnixEpochTime;
use simpledateformat::format_human2; use simpledateformat::format_human2;
use crate::{file, util}; use crate::{config, util, util_enc_file, util_envelop};
use crate::util::TINY_ENC_FILE_EXT; use crate::config::TinyEncryptConfig;
use crate::consts::{DATE_TIME_FORMAT, TINY_ENC_AES_GCM};
use crate::util::is_tiny_enc_file;
use crate::wrap_key::WrapKey;
#[derive(Debug, Args)] #[derive(Debug, Args)]
pub struct CmdInfo { pub struct CmdInfo {
/// Show raw meta
#[arg(long, short = 'M', default_value_t = false)]
pub raw_meta: bool,
/// Config file or based64 encoded (starts with: base64:)
#[arg(long)]
pub config: Option<String>,
/// File /// File
pub paths: Vec<PathBuf>, pub paths: Vec<PathBuf>,
/// Show raw meta
#[arg(long, default_value_t = false)]
pub raw_meta: bool,
} }
pub fn info(cmd_info: CmdInfo) -> XResult<()> { pub fn info(cmd_info: CmdInfo) -> XResult<()> {
let config = TinyEncryptConfig::load_default(&cmd_info.config).ok();
for (i, path) in cmd_info.paths.iter().enumerate() { for (i, path) in cmd_info.paths.iter().enumerate() {
let path = config::resolve_path_namespace(&config, path, true);
if i > 0 { println!("{}", "-".repeat(88)); } if i > 0 { println!("{}", "-".repeat(88)); }
if let Err(e) = info_single(path, &cmd_info) { if let Err(e) = info_single(&path, &cmd_info, &config) {
warning!("Parse Tiny Encrypt file info failed: {}", e); failure!("Parse Tiny Encrypt file info failed: {}", e);
} }
} }
println!();
Ok(()) Ok(())
} }
pub fn info_single(path: &PathBuf, cmd_info: &CmdInfo) -> XResult<()> { pub fn info_single(path: &PathBuf, cmd_info: &CmdInfo, config: &Option<TinyEncryptConfig>) -> XResult<()> {
let path_display = format!("{}", path.display()); let path_display = format!("{}", path.display());
if !path_display.ends_with(TINY_ENC_FILE_EXT) { if !is_tiny_enc_file(&path_display) {
return simple_error!("Not a Tiny Encrypt file: {}", path_display); return simple_error!("Not a Tiny Encrypt file: {}", path_display);
} }
let mut file_in = opt_result!(File::open(path), "Open file: {} failed: {}", &path_display); let mut file_in = opt_result!(File::open(path), "Open file: {} failed: {}", &path_display);
let meta = opt_result!( let file_in_len = file_in.metadata().map(|m| m.len()).unwrap_or(0);
file::read_tiny_encrypt_meta_and_normalize(&mut file_in), "Read file: {}, failed: {}", &path_display
let (meta_len, _, meta) = opt_result!(
util_enc_file::read_tiny_encrypt_meta_and_normalize(&mut file_in), "Read file: {}, failed: {}", &path_display
); );
if cmd_info.raw_meta { if cmd_info.raw_meta {
success!("Meta data:\n{}", serde_json::to_string_pretty(&meta).expect("SHOULD NOT HAPPEN")); println!("{}", serde_json::to_string_pretty(&meta).expect("SHOULD NOT HAPPEN"));
return Ok(()); return Ok(());
} }
@@ -50,64 +66,85 @@ pub fn info_single(path: &PathBuf, cmd_info: &CmdInfo) -> XResult<()> {
infos.push("Tiny Encrypt File Info".to_string()); infos.push("Tiny Encrypt File Info".to_string());
let compressed = if meta.compress { " [compressed]" } else { "" }; let compressed = if meta.compress { " [compressed]" } else { "" };
infos.push(format!("{}: {}{}", header("File name"), path_display, compressed)); infos.push(format!("{}: {}{}", header("File name"), path_display, compressed));
infos.push(format!("{}: {} bytes", header("File size"), meta.file_length));
if meta.compress && file_in_len > (2 + 4 + meta_len as u64) {
let actual_file_in_len = file_in_len - 2 - 4 - meta_len as u64;
infos.push(format!("{}: {}, after compressed {}, ratio: {}%",
header("File size"),
util_size::get_display_size(meta.file_length as i64),
util_size::get_display_size(actual_file_in_len as i64),
util::ratio(actual_file_in_len, meta.file_length)
));
} else {
infos.push(format!("{}: {}", header("File size"),
util_size::get_display_size(meta.file_length as i64)
));
}
infos.push(format!("{}: {}",
header("Meta size"), util_size::get_display_size(meta_len as i64))
);
infos.push(format!("{}: Version: {}, Agent: {}", infos.push(format!("{}: Version: {}, Agent: {}",
header("File summary"), meta.version, meta.user_agent) header("File summary"), meta.version, meta.user_agent)
); );
if let Some(latest_user_agent) = meta.latest_user_agent {
infos.push(format!("{}: {}", header("Latest user agent"), latest_user_agent))
}
let now_millis = util_time::get_current_millis() as u64; let now_millis = util_time::get_current_millis() as u64;
let fmt = simpledateformat::fmt("EEE MMM dd HH:mm:ss z yyyy").unwrap(); let fmt = simpledateformat::fmt(DATE_TIME_FORMAT).unwrap();
infos.push(format!("{}: {}, {} ago",
header("Last modified"),
fmt.format_local(from_unix_epoch(meta.file_last_modified)),
format_human2(Duration::from_millis(now_millis - meta.file_last_modified))
));
infos.push(format!("{}: {}, {} ago", infos.push(format!("{}: {}, {} ago",
header("Created"), header("Created"),
fmt.format_local(from_unix_epoch(meta.created)), fmt.format_local(SystemTime::from_millis(meta.created)),
format_human2(Duration::from_millis(now_millis - meta.created)) format_human2(Duration::from_millis(now_millis - meta.created))
)); ));
infos.push(format!("{}: {}, {} ago",
header("Last modified"),
fmt.format_local(SystemTime::from_millis(meta.file_last_modified)),
format_human2(Duration::from_millis(now_millis - meta.file_last_modified))
));
if let Some(file_edit_count) = meta.file_edit_count {
infos.push(format!("{}: {} time(s)",
header("Edit count"),
file_edit_count
));
}
meta.envelops.as_ref().map(|envelops| if let Some(envelops) = meta.envelops.as_ref() {
envelops.iter().enumerate().for_each(|(i, envelop)| { envelops.iter().enumerate().for_each(|(i, envelop)| {
let kid = iff!(envelop.kid.is_empty(), "".into(), format!(", Kid: {}", envelop.kid)); infos.push(format!("{}: {}",
let desc = envelop.desc.as_ref().map(|desc| format!(", Desc: {}", desc)).unwrap_or_else(|| "".to_string());
infos.push(format!("{}: {}{}{}",
header(&format!("Envelop #{}", i + 1)), header(&format!("Envelop #{}", i + 1)),
envelop.r#type.get_upper_name(), util_envelop::format_envelop(envelop, config)
kid,
desc
)); ));
util_msg::when_debug(|| {
if let Ok(wrap_key) = WrapKey::parse(&envelop.encrypted_key) {
debugging!("Wrap key: {}", serde_json::to_string(&wrap_key).expect("SHOULD NOT HAPPEN"));
}
});
}) })
); }
meta.pgp_fingerprint.map(|fingerprint| {
if let Some(fingerprint) = meta.pgp_fingerprint {
infos.push(format!("{}: {}", header("PGP fingerprint"), fingerprint)); infos.push(format!("{}: {}", header("PGP fingerprint"), fingerprint));
}); }
meta.comment.map(|comment| { if let Some(comment) = meta.comment {
infos.push(format!("{}: {}", header("Comment"), comment)); infos.push(format!("{}: {}", header("Comment"), comment));
}); }
infos.push(format!("{}: {}", header("Encrypted comment"), to_yes_or_no(&meta.encrypted_comment))); infos.push(format!("{}: {}", header("Encrypted comment"), to_yes_or_no(&meta.encrypted_comment)));
infos.push(format!("{}: {}", header("Encrypted meta"), to_yes_or_no(&meta.encrypted_meta))); infos.push(format!("{}: {}", header("Encrypted meta"), to_yes_or_no(&meta.encrypted_meta)));
let encryption_algorithm = if let Some(encryption_algorithm) = &meta.encryption_algorithm { let encryption_algorithm = meta.encryption_algorithm.clone()
encryption_algorithm.to_string() .unwrap_or_else(|| format!("{} (default)", TINY_ENC_AES_GCM));
} else {
format!("{} (default)", util::TINY_ENC_AES_GCM)
};
infos.push(format!("{}: {}", header("Encryption algorithm"), encryption_algorithm)); infos.push(format!("{}: {}", header("Encryption algorithm"), encryption_algorithm));
success!("{}\n", infos.join("\n")); success!("{}", infos.join("\n"));
Ok(()) Ok(())
} }
fn from_unix_epoch(t: u64) -> SystemTime {
SystemTime::UNIX_EPOCH.add(Duration::from_millis(t))
}
fn header(h: &str) -> String { fn header(h: &str) -> String {
let width = 21; let width = 21;
h.to_string() + ".".repeat(max(width - h.len(), 0)).as_str() h.to_string() + ".".repeat(max(width - h.len(), 0)).as_str()
} }
fn to_yes_or_no(opt: &Option<String>) -> String { fn to_yes_or_no(opt: &Option<String>) -> String {
opt.as_ref().map(|_| "YES".to_string()).unwrap_or_else(|| "NO".to_string()) iff!(opt.is_some(), "YES", "NO").to_string()
} }

185
src/cmd_initkeychain.rs Normal file
View File

@@ -0,0 +1,185 @@
use clap::Args;
use pqcrypto_traits::kem::PublicKey;
use rust_util::{debugging, information, opt_result, simple_error, success, warning, XResult};
use swift_secure_enclave_tool_rs::ControlFlag;
use crate::config::TinyEncryptConfigEnvelop;
use crate::spec::TinyEncryptEnvelopType;
use crate::util_keychainkey;
use crate::util_keychainstatic;
use crate::util_keychainstatic::{KeychainKey, KeychainStaticSecret, KeychainStaticSecretAlgorithm};
#[derive(Debug, Args)]
pub struct CmdInitKeychain {
/// Secure Enclave
#[arg(long, short = 'S')]
pub secure_enclave: bool,
/// Secure Enclave control flag, e.g. none, user-presence, device-passcode, biometry-any, biometry-current-set
#[arg(long, short = 'C')]
pub secure_enclave_control_flag: Option<String>,
/// Expose secure enclave private key data
#[arg(long, short = 'E')]
pub expose_secure_enclave_private_key: bool,
/// Keychain name, or default [--keychain-name not works yet]
#[arg(long, short = 'c')]
pub keychain_name: Option<String>,
/// Service name, or default: tiny-encrypt
#[arg(long, short = 's')]
pub server_name: Option<String>,
/// Key name
#[arg(long, short = 'n')]
pub key_name: String,
/// Algorithm (x25519, or kyber1024, default x25519)
#[arg(long, short = 'a')]
pub algorithm: Option<String>,
}
const DEFAULT_SERVICE_NAME: &str = "tiny-encrypt";
pub fn init_keychain(cmd_init_keychain: CmdInitKeychain) -> XResult<()> {
if cmd_init_keychain.secure_enclave {
keychain_key_se(cmd_init_keychain)
} else {
keychain_key_static(cmd_init_keychain)
}
}
pub fn keychain_key_se(cmd_init_keychain: CmdInitKeychain) -> XResult<()> {
if !util_keychainkey::is_support_se() {
return simple_error!("Secure enclave is not supported.");
}
let keychain_name = cmd_init_keychain.keychain_name.as_deref().unwrap_or("");
let service_name = cmd_init_keychain.server_name.as_deref().unwrap_or(DEFAULT_SERVICE_NAME);
let key_name = &cmd_init_keychain.key_name;
let control_flag = match &cmd_init_keychain.secure_enclave_control_flag {
None => return simple_error!("Parameter --secure-enclave-control-flag required"),
Some(control_flag) => match control_flag.as_str() {
"none" => ControlFlag::None,
"user-presence" | "up" => ControlFlag::UserPresence,
"device-passcode" | "passcode" | "pass" => ControlFlag::DevicePasscode,
"biometry-any" | "bio-any" => ControlFlag::BiometryAny,
"biometry-current-set" | "bio-current" => ControlFlag::BiometryCurrentSet,
_ => return simple_error!("Invalid control flag: {}", control_flag),
}
};
let (public_key_hex, private_key_base64) = util_keychainkey::generate_se_p256_keypair(control_flag)?;
let public_key_compressed_hex = public_key_hex.chars()
.skip(2).take(public_key_hex.len() / 2 - 1).collect::<String>();
let saved_arg0 = if cmd_init_keychain.expose_secure_enclave_private_key {
private_key_base64
} else {
let keychain_key = KeychainKey::from(keychain_name, service_name, key_name);
keychain_key.set_password(private_key_base64.as_bytes())?;
keychain_key.to_str()
};
let config_envelop = TinyEncryptConfigEnvelop {
r#type: TinyEncryptEnvelopType::KeyP256,
sid: Some(cmd_init_keychain.key_name.clone()),
kid: format!("keychain:02{}", &public_key_compressed_hex),
desc: Some("Keychain Secure Enclave".to_string()),
args: Some(vec![saved_arg0]),
public_part: public_key_hex,
profiles: None,
};
information!("Config envelop:\n{}", serde_json::to_string_pretty(&config_envelop).unwrap());
Ok(())
}
pub fn keychain_key_static(cmd_init_keychain: CmdInitKeychain) -> XResult<()> {
let keychain_name = cmd_init_keychain.keychain_name.as_deref().unwrap_or("");
let service_name = cmd_init_keychain.server_name.as_deref().unwrap_or(DEFAULT_SERVICE_NAME);
let key_name = &cmd_init_keychain.key_name;
let keychain_key = KeychainKey::from(keychain_name, service_name, key_name);
let mut envelop_type = match &cmd_init_keychain.algorithm {
None => TinyEncryptEnvelopType::StaticX25519,
Some(algorithm) => {
let a_lower = algorithm.to_lowercase();
if &a_lower == "kyber" || &a_lower == "kyber1024" {
TinyEncryptEnvelopType::StaticKyber1024
} else if &a_lower == "25519" || &a_lower == "x25519" || &a_lower == "cv25519" || &a_lower == "curve25519" {
TinyEncryptEnvelopType::StaticX25519
} else {
return simple_error!("Unknown algorithm: {}", algorithm);
}
}
};
let public_key_hex = match keychain_key.get_password()? {
Some(static_key) => {
warning!("Key already exists: {}.{}", service_name, key_name);
let keychain_static_secret = KeychainStaticSecret::parse_bytes(static_key.as_ref())?;
match keychain_static_secret.algo {
KeychainStaticSecretAlgorithm::X25519 => {
envelop_type = TinyEncryptEnvelopType::StaticX25519;
}
KeychainStaticSecretAlgorithm::Kyber1024 => {
envelop_type = TinyEncryptEnvelopType::StaticKyber1024;
}
}
match keychain_static_secret.algo {
KeychainStaticSecretAlgorithm::X25519 => {
let public_key = keychain_static_secret.to_x25519_public_key()?;
hex::encode(public_key.as_bytes())
}
KeychainStaticSecretAlgorithm::Kyber1024 => {
let (_, public_key) = keychain_static_secret.to_kyber1204_static_secret()?;
hex::encode(public_key.as_bytes())
}
}
}
None => {
let (keychain_key_bytes, public_key_hex) = match envelop_type {
TinyEncryptEnvelopType::StaticX25519 => {
let (keychain_key_bytes, public_key) = util_keychainstatic::generate_static_x25519_secret();
(keychain_key_bytes, hex::encode(public_key.as_bytes()))
}
TinyEncryptEnvelopType::StaticKyber1024 => {
let (keychain_key_bytes, public_key) = util_keychainstatic::generate_static_kyber1024_secret();
(keychain_key_bytes, hex::encode(public_key.as_bytes()))
}
_ => unreachable!(),
};
debugging!("Keychain key : {}", keychain_key_bytes);
opt_result!(
keychain_key.set_password(keychain_key_bytes.as_bytes()),
"Write static key failed: {}"
);
public_key_hex
}
};
success!("Keychain name: {}", &key_name);
success!("Public key : {}", &public_key_hex);
let kid_part2 = if public_key_hex.len() <= 64 {
public_key_hex.clone()
} else {
public_key_hex.chars().take(64).collect()
};
let config_envelop = TinyEncryptConfigEnvelop {
r#type: envelop_type,
sid: Some(key_name.clone()),
kid: format!("keychain:{}", &kid_part2),
desc: Some("Keychain static".to_string()),
args: Some(vec![keychain_key.to_str()]),
public_part: public_key_hex,
profiles: None,
};
information!("Config envelop:\n{}", serde_json::to_string_pretty(&config_envelop).unwrap());
Ok(())
}

145
src/cmd_initpiv.rs Normal file
View File

@@ -0,0 +1,145 @@
use clap::Args;
use p256::pkcs8::der::Decode;
use rust_util::{failure, iff, information, opt_result, simple_error, warning, XResult};
use spki::{ObjectIdentifier, SubjectPublicKeyInfoOwned};
use spki::der::Encode;
use x509_parser::prelude::FromDer;
use x509_parser::public_key::RSAPublicKey;
use yubikey::Certificate;
use yubikey::Key;
use yubikey::piv::{AlgorithmId, SlotId};
use yubikey::YubiKey;
use crate::config::TinyEncryptConfigEnvelop;
use crate::spec::TinyEncryptEnvelopType;
use crate::{util, util_piv};
use crate::util_digest::sha256_digest;
#[derive(Debug, Args)]
pub struct CmdInitPiv {
/// PIV slot
#[arg(long, short = 's')]
pub slot: String,
}
const RSA: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.1");
const ECC: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.10045.2.1");
const ECC_P256: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.10045.3.1.7");
const ECC_P384: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.132.0.34");
pub fn init_piv(cmd_init_piv: CmdInitPiv) -> XResult<()> {
let mut yk = opt_result!(YubiKey::open(), "YubiKey not found: {}");
let slot_id = util_piv::get_slot_id(&cmd_init_piv.slot)?;
let slot_id_hex = to_slot_hex(&slot_id);
let keys = opt_result!(Key::list(&mut yk), "List keys failed: {}");
let find_key = || {
for k in &keys {
let key_slot_str = format!("{:x}", Into::<u8>::into(k.slot()));
if slot_equals(&slot_id, &key_slot_str) {
return Some(k);
}
}
None
};
let key = match find_key() {
None => {
warning!("Key not found.");
return Ok(());
}
Some(key) => key,
};
let cert = &key.certificate().cert.tbs_certificate;
if let Ok(algorithm_id) = get_algorithm_id_by_certificate(key.certificate()) {
let public_key_bit_string = &cert.subject_public_key_info.subject_public_key;
match algorithm_id {
AlgorithmId::EccP256 | AlgorithmId::EccP384 => {
let pk_point_hex = public_key_bit_string.raw_bytes();
let public_key_point_hex = hex::encode(pk_point_hex);
let compressed_public_key_point_hex = format!("02{}", hex::encode(&pk_point_hex[1..(pk_point_hex.len() / 2) + 1]));
let is_p256 = algorithm_id == AlgorithmId::EccP256;
let config_envelop = TinyEncryptConfigEnvelop {
r#type: iff!(is_p256, TinyEncryptEnvelopType::PivP256, TinyEncryptEnvelopType::PivP384),
sid: Some(format!("piv-{}-ecdh-{}", &slot_id_hex, iff!(is_p256, "p256", "p384"))),
kid: compressed_public_key_point_hex.clone(),
desc: Some(format!("PIV --slot {}", &slot_id_hex)),
args: Some(vec![
slot_id_hex.clone()
]),
public_part: public_key_point_hex,
profiles: None,
};
information!("Config envelop:\n{}", serde_json::to_string_pretty(&config_envelop).unwrap());
}
AlgorithmId::Rsa2048 => {
let spki = opt_result!(cert.subject_public_key_info.to_der(), "Generate SPKI DER failed: {}");
let config_envelop = TinyEncryptConfigEnvelop {
r#type: TinyEncryptEnvelopType::PivRsa,
sid: Some(format!("piv-{}-rsa2048", &slot_id_hex)),
kid: format!("piv:{}", hex::encode(sha256_digest(&spki))),
desc: Some(format!("PIV --slot {}", &slot_id_hex)),
args: Some(vec![
slot_id_hex.clone()
]),
public_part: util::to_pem(&spki, "PUBLIC KEY"),
profiles: None,
};
information!("Config envelop:\n{}", serde_json::to_string_pretty(&config_envelop).unwrap());
}
_ => {
failure!("Only support P256, P384 or RSA2048, actual: {:?}", algorithm_id);
}
}
}
Ok(())
}
fn get_algorithm_id_by_certificate(certificate: &Certificate) -> XResult<AlgorithmId> {
let tbs_certificate = &certificate.cert.tbs_certificate;
get_algorithm_id(&tbs_certificate.subject_public_key_info)
}
fn get_algorithm_id(public_key_info: &SubjectPublicKeyInfoOwned) -> XResult<AlgorithmId> {
if public_key_info.algorithm.oid == RSA {
let rsa_public_key = opt_result!(
RSAPublicKey::from_der(public_key_info.subject_public_key.raw_bytes()), "Parse public key failed: {}");
let starts_with_0 = rsa_public_key.1.modulus.starts_with(&[0]);
let public_key_bits = (rsa_public_key.1.modulus.len() - iff!(starts_with_0, 1, 0)) * 8;
if public_key_bits == 1024 {
return Ok(AlgorithmId::Rsa1024);
}
if public_key_bits == 2048 {
return Ok(AlgorithmId::Rsa2048);
}
return simple_error!("Unknown rsa bits: {}", public_key_bits);
}
if public_key_info.algorithm.oid == ECC {
if let Some(any) = &public_key_info.algorithm.parameters {
let any_parameter_der = opt_result!(any.to_der(), "Bad any parameter: {}");
let any_parameter_oid = opt_result!(ObjectIdentifier::from_der(&any_parameter_der), "Bad any parameter der: {}");
if any_parameter_oid == ECC_P256 {
return Ok(AlgorithmId::EccP256);
}
if any_parameter_oid == ECC_P384 {
return Ok(AlgorithmId::EccP384);
}
return simple_error!("Unknown any parameter oid: {}", any_parameter_oid);
}
}
simple_error!("Unknown algorithm: {}", public_key_info.algorithm.oid)
}
fn slot_equals(slot_id: &SlotId, slot: &str) -> bool {
util_piv::get_slot_id(slot).map(|sid| &sid == slot_id).unwrap_or(false)
}
fn to_slot_hex(slot: &SlotId) -> String {
let slot_id: u8 = (*slot).into();
format!("{:x}", slot_id)
}

View File

@@ -0,0 +1,334 @@
use crate::config::TinyEncryptConfig;
use crate::spec::TinyEncryptEnvelop;
use crate::{cmd_encrypt, crypto_cryptor, util, util_env};
use base64::engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD};
use base64::Engine;
use clap::Args;
use rust_util::{debugging, opt_result, simple_error, XResult};
use serde::Serialize;
use std::io;
use std::io::Write;
use std::process::exit;
use crate::temporary_key::parse_temporary_keys;
use crate::util_simple_pbe::SimplePbkdfEncryptionV1;
// Reference: https://git.hatter.ink/hatter/tiny-encrypt-rs/issues/3
const SIMPLE_ENCRYPTION_HEADER: &str = "tinyencrypt-dir";
const SIMPLE_ENCRYPTION_DOT: &str = ".";
#[derive(Debug, Args)]
pub struct CmdSimpleEncrypt {
/// Encryption profile (use default when --key-filter is assigned)
#[arg(long, short = 'p')]
pub profile: Option<String>,
/// Encryption key filter (key_id or type:TYPE(e.g. ecdh, pgp, ecdh-p384, pgp-ed25519), multiple joined by ',', ALL for all)
#[arg(long, short = 'k')]
pub key_filter: Option<String>,
/// Temporary key
#[arg(long)]
pub temporary_key: Option<Vec<String>>,
/// Encrypt value from stdin
#[arg(long)]
pub value_stdin: bool,
/// Encrypt value
#[arg(long, short = 'v')]
pub value: Option<String>,
/// Encrypt value in bse64
#[arg(long)]
pub value_base64: Option<String>,
/// Encrypt value in hex
#[arg(long)]
pub value_hex: Option<String>,
/// With PBKDF encryption
#[arg(long, short = 'P')]
pub with_pbkdf_encryption: bool,
/// PBKDF iterations (default: 10000)
#[arg(long, short = 'i')]
pub pbkdf_iterations: Option<u32>,
/// PBKDF encryption password
#[arg(long, short = 'A')]
pub password: Option<String>,
/// Config file or based64 encoded (starts with: base64:)
#[arg(long)]
pub config: Option<String>,
/// Direct output result value
#[arg(long)]
pub outputs_password: bool,
/// Direct output result value
#[arg(long)]
pub direct_output: bool,
}
#[derive(Debug, Args)]
pub struct CmdSimpleDecrypt {
/// PGP or PIV PIN
#[arg(long, short = 'p')]
pub pin: Option<String>,
/// Decrypt key ID
#[arg(long, short = 'k')]
pub key_id: Option<String>,
/// PIV slot
#[arg(long, short = 's')]
pub slot: Option<String>,
/// Decrypt value from stdin
#[arg(long)]
pub value_stdin: bool,
/// Decrypt value
#[arg(long, short = 'v')]
pub value: Option<String>,
/// Decrypt result output format (plain, hex, bse64)
#[arg(long, short = 'o')]
pub output_format: Option<String>,
/// PBKDF encryption password
#[arg(long, short = 'A')]
pub password: Option<String>,
/// Config file or based64 encoded (starts with: base64:)
#[arg(long)]
pub config: Option<String>,
/// Direct output result value
#[arg(long)]
pub outputs_password: bool,
/// Direct output result value
#[arg(long)]
pub direct_output: bool,
}
impl CmdSimpleEncrypt {
pub fn get_value(&self) -> XResult<Option<Vec<u8>>> {
if self.value_stdin {
return Ok(Some(util::read_stdin()?));
}
if let Some(value) = &self.value {
return Ok(Some(value.as_bytes().to_vec()));
}
if let Some(value_base64) = &self.value_base64 {
return Ok(Some(opt_result!(STANDARD.decode(value_base64), "Parse value base64 failed: {}")));
}
if let Some(value_hex) = &self.value_hex {
return Ok(Some(opt_result!(hex::decode(value_hex), "Parse value hex failed: {}")));
}
Ok(None)
}
}
impl CmdSimpleDecrypt {
pub fn get_value(&self) -> XResult<Option<String>> {
if self.value_stdin {
return Ok(Some(opt_result!(String::from_utf8(util::read_stdin()?), "Read stdin value failed: {}")));
}
Ok(self.value.clone())
}
}
#[derive(Serialize)]
pub struct CmdResult {
pub code: i32,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub password: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<String>,
}
impl CmdResult {
pub fn fail(code: i32, message: &str) -> Self {
Self {
code,
message: Some(message.to_string()),
password: None,
result: None,
}
}
pub fn success(result: &str, password: Option<String>) -> Self {
Self {
code: 0,
message: None,
password,
result: Some(result.to_string()),
}
}
pub fn print_exit(&self, direct_output_value: bool) -> ! {
// TODO direct_output_value
if direct_output_value {
if self.code == 0 {
print!("{}", self.result.as_deref().unwrap());
} else {
println!("{}", self.message.as_deref().unwrap_or("unknown error"));
}
} else {
let result = serde_json::to_string_pretty(self).unwrap();
println!("{}", result);
}
exit(self.code)
}
}
pub fn simple_encrypt(cmd_simple_encrypt: CmdSimpleEncrypt) -> XResult<()> {
let direct_output = cmd_simple_encrypt.direct_output;
if let Err(inner_result_error) = inner_simple_encrypt(cmd_simple_encrypt) {
CmdResult::fail(-1, &format!("{}", inner_result_error)).print_exit(direct_output);
}
Ok(())
}
#[cfg(feature = "decrypt")]
pub fn simple_decrypt(cmd_simple_decrypt: CmdSimpleDecrypt) -> XResult<()> {
let direct_output = cmd_simple_decrypt.direct_output;
if let Err(inner_result_error) = inner_simple_decrypt(cmd_simple_decrypt) {
CmdResult::fail(-1, &format!("{}", inner_result_error)).print_exit(direct_output);
}
Ok(())
}
pub fn inner_simple_encrypt(cmd_simple_encrypt: CmdSimpleEncrypt) -> XResult<()> {
let config = TinyEncryptConfig::load_default(&cmd_simple_encrypt.config)?;
debugging!("Found tiny encrypt config: {:?}", config);
let mut envelops = config.find_envelops(
&cmd_simple_encrypt.profile,
&cmd_simple_encrypt.key_filter)?;
debugging!("Found envelops: {:?}", envelops);
let temporary_envelops = parse_temporary_keys(&cmd_simple_encrypt.temporary_key)?;
if !temporary_envelops.is_empty() {
for t_envelop in &temporary_envelops {
envelops.push(t_envelop)
}
debugging!("Final envelops: {:?}", envelops);
}
if envelops.is_empty() { return simple_error!("Cannot find any valid envelops"); }
let envelop_tkids: Vec<_> = envelops.iter()
.map(|e| format!("{}:{}", e.r#type.get_name(), e.kid))
.collect();
debugging!("Matched {} envelop(s): \n- {}", envelops.len(), envelop_tkids.join("\n- "));
if envelop_tkids.is_empty() {
return simple_error!("no matched envelops found");
}
let value = match cmd_simple_encrypt.get_value()? {
None => return simple_error!("--value-stdin/value/value-base64/value-hex must assign one"),
Some(value) => value,
};
let cryptor = crypto_cryptor::get_cryptor_by_encryption_algorithm(&None)?;
let envelops = cmd_encrypt::encrypt_envelops(cryptor, &value, &envelops)?;
let envelops_json = serde_json::to_string(&envelops)?;
let mut simple_encrypt_result = format!("{}.{}",
SIMPLE_ENCRYPTION_HEADER,
URL_SAFE_NO_PAD.encode(envelops_json.as_bytes())
);
let with_pbkdf_encryption = cmd_simple_encrypt.with_pbkdf_encryption || cmd_simple_encrypt.password.is_some();
let mut outputs_password = None;
if with_pbkdf_encryption {
let password = util::read_password(&cmd_simple_encrypt.password)?;
simple_encrypt_result = SimplePbkdfEncryptionV1::encrypt(&password, simple_encrypt_result.as_bytes(),
&cmd_simple_encrypt.pbkdf_iterations)?.to_string();
if cmd_simple_encrypt.outputs_password {
outputs_password = Some(password);
}
}
CmdResult::success(&simple_encrypt_result, outputs_password).print_exit(cmd_simple_encrypt.direct_output);
}
#[cfg(feature = "decrypt")]
pub fn inner_simple_decrypt(cmd_simple_decrypt: CmdSimpleDecrypt) -> XResult<()> {
let config = TinyEncryptConfig::load_default(&cmd_simple_decrypt.config).ok();
let pin = cmd_simple_decrypt.pin.clone().or_else(util_env::get_pin);
let slot = cmd_simple_decrypt.slot.clone();
let output_format = cmd_simple_decrypt.output_format.as_deref().unwrap_or("plain");
match output_format {
"plain" | "hex" | "base64" => (),
_ => return simple_error!("not supported output format: {}", output_format),
};
let mut value = match cmd_simple_decrypt.get_value()? {
None => return simple_error!("--value-stdin/value must assign one"),
Some(value) => value,
};
let mut outputs_password = None;
if SimplePbkdfEncryptionV1::matches(&value) {
let simple_pbkdf_encryption_v1: SimplePbkdfEncryptionV1 = value.as_str().try_into()?;
let password = util::read_password(&cmd_simple_decrypt.password)?;
let plaintext_bytes = simple_pbkdf_encryption_v1.decrypt(&password)?;
value = opt_result!(String::from_utf8(plaintext_bytes), "Decrypt PBKDF encryption failed: {}");
if cmd_simple_decrypt.outputs_password {
outputs_password = Some(password);
}
}
let value_parts = value.trim().split(SIMPLE_ENCRYPTION_DOT).collect::<Vec<_>>();
if value_parts.len() != 2 {
return simple_error!("bad value format: {}", value);
}
if value_parts[0] != SIMPLE_ENCRYPTION_HEADER {
return simple_error!("bad value format: {}", value);
}
let envelopes_json = opt_result!(URL_SAFE_NO_PAD.decode(value_parts[1]), "bad value format: {}");
let envelops: Vec<TinyEncryptEnvelop> = match serde_json::from_slice(&envelopes_json) {
Err(_) => return simple_error!("bad value format: {}", value),
Ok(value) => value,
};
let filter_envelops = envelops.iter().filter(|e| {
match &cmd_simple_decrypt.key_id {
None => true,
Some(key_id) => &e.kid == key_id,
}
}).collect::<Vec<_>>();
if filter_envelops.is_empty() {
return simple_error!("no envelops found: {:?}", cmd_simple_decrypt.key_id);
}
if filter_envelops.len() > 1 {
let mut kids = vec![];
debugging!("Found {} envelopes", filter_envelops.len());
for envelop in &filter_envelops {
kids.push(envelop.kid.clone());
debugging!("- {} {}", envelop.kid, envelop.r#type.get_name());
}
return simple_error!("too many envelops: {:?}, len: {}, matched kids: [{}]", cmd_simple_decrypt.key_id, filter_envelops.len(), kids.join(","));
}
let value = crate::cmd_decrypt::try_decrypt_key(&config, filter_envelops[0], &pin, &slot, false)?;
if cmd_simple_decrypt.direct_output && output_format == "plain" {
io::stdout().write_all(&value).expect("unable to write to stdout");
exit(0);
}
let value = match output_format {
"plain" => opt_result!(String::from_utf8(value), "bad value encoding: {}"),
"hex" => hex::encode(&value),
"base64" => STANDARD.encode(&value),
_ => return simple_error!("not supported output format: {}", output_format),
};
CmdResult::success(&value, outputs_password).print_exit(cmd_simple_decrypt.direct_output);
}

27
src/cmd_version.rs Normal file
View File

@@ -0,0 +1,27 @@
use clap::Args;
use rust_util::{iff, XResult};
use crate::util;
use crate::util_keychainkey;
#[derive(Debug, Args)]
pub struct CmdVersion {}
pub fn version(_cmd_version: CmdVersion) -> XResult<()> {
let mut features: Vec<String> = vec![];
#[cfg(feature = "decrypt")]
features.push("decrypt".to_string());
#[cfg(feature = "macos")]
features.push("macos".to_string());
#[cfg(feature = "smartcard")]
features.push("smartcard".to_string());
if features.is_empty() { features.push("-".to_string()); }
println!(
"User-Agent: {} [with features: {}]{}\n{}",
util::get_user_agent(),
features.join(", "),
iff!(util_keychainkey::is_support_se(), " with Secure Enclave Supported", ""),
env!("CARGO_PKG_DESCRIPTION")
);
Ok(())
}

View File

@@ -2,7 +2,27 @@ use std::io::Write;
use flate2::Compression; use flate2::Compression;
use flate2::write::{GzDecoder, GzEncoder}; use flate2::write::{GzDecoder, GzEncoder};
use rust_util::{simple_error, XResult}; use rust_util::{opt_result, XResult};
const BUFFER_SIZE: usize = 8 * 1024;
pub fn compress_default(message: &[u8]) -> XResult<Vec<u8>> {
compress(Compression::default(), message)
}
pub fn compress(compression: Compression, message: &[u8]) -> XResult<Vec<u8>> {
let mut encoder = GzStreamEncoder::new(compression);
let mut buff = encoder.update(message)?;
buff.extend_from_slice(&encoder.finalize()?);
Ok(buff)
}
pub fn decompress(message: &[u8]) -> XResult<Vec<u8>> {
let mut decoder = GzStreamDecoder::new();
let mut buff = decoder.update(message)?;
buff.extend_from_slice(&decoder.finalize()?);
Ok(buff)
}
pub struct GzStreamEncoder { pub struct GzStreamEncoder {
gz_encoder: GzEncoder<Vec<u8>>, gz_encoder: GzEncoder<Vec<u8>>,
@@ -14,13 +34,13 @@ impl GzStreamEncoder {
} }
pub fn new(compression: Compression) -> Self { pub fn new(compression: Compression) -> Self {
let buffer = Vec::with_capacity(1024 * 8); let buffer = Vec::with_capacity(BUFFER_SIZE);
let gz_encoder = GzEncoder::new(buffer, compression); let gz_encoder = GzEncoder::new(buffer, compression);
Self { gz_encoder } Self { gz_encoder }
} }
pub fn update(&mut self, buff: &[u8]) -> XResult<Vec<u8>> { pub fn update(&mut self, buff: &[u8]) -> XResult<Vec<u8>> {
self.gz_encoder.write_all(buff)?; opt_result!(self.gz_encoder.write_all(buff), "Encode Gz stream failed: {}");
let inner = self.gz_encoder.get_mut(); let inner = self.gz_encoder.get_mut();
let result = inner.clone(); let result = inner.clone();
inner.clear(); inner.clear();
@@ -28,10 +48,7 @@ impl GzStreamEncoder {
} }
pub fn finalize(self) -> XResult<Vec<u8>> { pub fn finalize(self) -> XResult<Vec<u8>> {
match self.gz_encoder.finish() { Ok(opt_result!(self.gz_encoder.finish(), "Encode Gz stream failed: {}"))
Ok(last_buffer) => Ok(last_buffer),
Err(e) => simple_error!("Decode stream failed: {}", e),
}
} }
} }
@@ -41,13 +58,13 @@ pub struct GzStreamDecoder {
impl GzStreamDecoder { impl GzStreamDecoder {
pub fn new() -> Self { pub fn new() -> Self {
let buffer = Vec::with_capacity(1024 * 8); let buffer = Vec::with_capacity(BUFFER_SIZE);
let gz_decoder = GzDecoder::new(buffer); let gz_decoder = GzDecoder::new(buffer);
Self { gz_decoder } Self { gz_decoder }
} }
pub fn update(&mut self, buff: &[u8]) -> XResult<Vec<u8>> { pub fn update(&mut self, buff: &[u8]) -> XResult<Vec<u8>> {
self.gz_decoder.write_all(buff)?; opt_result!(self.gz_decoder.write_all(buff), "Decode Gz stream failed: {}");
let inner = self.gz_decoder.get_mut(); let inner = self.gz_decoder.get_mut();
let result = inner.clone(); let result = inner.clone();
inner.clear(); inner.clear();
@@ -55,10 +72,7 @@ impl GzStreamDecoder {
} }
pub fn finalize(self) -> XResult<Vec<u8>> { pub fn finalize(self) -> XResult<Vec<u8>> {
match self.gz_decoder.finish() { Ok(opt_result!(self.gz_decoder.finish(), "Decode Gz stream failed: {}"))
Ok(last_buffer) => Ok(last_buffer),
Err(e) => simple_error!("Decode stream failed: {}", e),
}
} }
} }
@@ -82,7 +96,8 @@ fn test_gzip_compress() {
#[test] #[test]
fn test_gzip_compress_multi_blocks() { fn test_gzip_compress_multi_blocks() {
let compressed = hex::decode("1f8b0800000000000000f348cdc9c95708cf2fca49f12081090044f4575937000000").unwrap(); let compressed = hex::decode(
"1f8b0800000000000000f348cdc9c95708cf2fca49f12081090044f4575937000000").unwrap();
let decompressed = "Hello WorldHello WorldHello WorldHello WorldHello World"; let decompressed = "Hello WorldHello WorldHello WorldHello WorldHello World";
let mut decoder = GzStreamDecoder::new(); let mut decoder = GzStreamDecoder::new();
let mut decompressed_bytes = vec![]; let mut decompressed_bytes = vec![];

View File

@@ -1,12 +1,15 @@
use std::cmp::Ordering; use std::cmp::Ordering;
use std::collections::HashMap; use std::collections::HashMap;
use std::fs; use std::path::Path;
use std::path::PathBuf;
use rust_util::{debugging, opt_result, simple_error, XResult}; use std::{env, fs};
use rust_util::{util_env as rust_util_env};
use rust_util::util_file::resolve_file_path; use rust_util::util_file::resolve_file_path;
use rust_util::{debugging, opt_result, warning, XResult};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::consts::{ENV_TINY_ENC_CONFIG_FILE, TINY_ENC_CONFIG_FILE, TINY_ENC_CONFIG_FILE_2, TINY_ENC_CONFIG_FILE_3, TINY_ENC_FILE_EXT};
use crate::spec::TinyEncryptEnvelopType; use crate::spec::TinyEncryptEnvelopType;
use crate::util::decode_base64;
/// Config file sample: /// Config file sample:
/// ~/.tinyencrypt/config-rs.json /// ~/.tinyencrypt/config-rs.json
@@ -20,6 +23,7 @@ use crate::spec::TinyEncryptEnvelopType;
/// }, /// },
/// { /// {
/// "type": "ecdh", /// "type": "ecdh",
/// "sid": "SHORT-ID-1",
/// "kid": "KID-2", /// "kid": "KID-2",
/// "desc": "this is key 002", /// "desc": "this is key 002",
/// "publicPart": "04..." /// "publicPart": "04..."
@@ -33,76 +37,430 @@ use crate::spec::TinyEncryptEnvelopType;
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct TinyEncryptConfig { pub struct TinyEncryptConfig {
pub environment: Option<HashMap<String, StringOrVecString>>,
pub namespaces: Option<HashMap<String, String>>,
pub includes: Option<String>, // find all *.tinyencrypt.json
pub envelops: Vec<TinyEncryptConfigEnvelop>, pub envelops: Vec<TinyEncryptConfigEnvelop>,
pub profiles: HashMap<String, Vec<String>>, pub profiles: Option<HashMap<String, Vec<String>>>,
}
impl TinyEncryptConfig {
fn get_profile(&self, profile: &str) -> Option<&Vec<String>> {
match &self.profiles {
Some(profiles) => profiles.get(profile),
None => None,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub enum StringOrVecString {
String(String),
Vec(Vec<String>),
} }
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct TinyEncryptConfigEnvelop { pub struct TinyEncryptConfigEnvelop {
pub r#type: TinyEncryptEnvelopType, pub r#type: TinyEncryptEnvelopType,
#[serde(skip_serializing_if = "Option::is_none")]
pub sid: Option<String>,
pub kid: String, pub kid: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub desc: Option<String>, pub desc: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub args: Option<Vec<String>>, pub args: Option<Vec<String>>,
pub public_part: String, pub public_part: String,
pub profiles: Option<Vec<String>>,
} }
impl TinyEncryptConfig { impl TinyEncryptConfig {
pub fn load(file: &str) -> XResult<Self> { pub fn load_default(config: &Option<String>) -> XResult<Self> {
let resolved_file0 = config.clone().or_else(|| rust_util_env::env_var(ENV_TINY_ENC_CONFIG_FILE));
let resolved_file_1 = resolve_file_path(TINY_ENC_CONFIG_FILE);
let resolved_file_2 = resolve_file_path(TINY_ENC_CONFIG_FILE_2);
let resolved_file_3 = resolve_file_path(TINY_ENC_CONFIG_FILE_3);
if let Some(resolved_file) = resolved_file0 {
if resolved_file.starts_with("base64:") {
let decoded_resolved_bytes_result = decode_base64(&resolved_file.chars().skip(7).collect::<String>());
let decoded_resolved_bytes = opt_result!(decoded_resolved_bytes_result, "Decode base64 failed: {}");
let decoded_resolved_content = opt_result!(String::from_utf8(decoded_resolved_bytes), "Decode UTF-8 string failed: {}");
return Self::load_content(&decoded_resolved_content, "<env>");
}
debugging!("Found tiny encrypt config file: {}", &resolved_file);
return Self::load_file(&resolved_file)
}
let config_file = if fs::metadata(&resolved_file_1).is_ok() {
debugging!("Load config from: {resolved_file_1}");
resolved_file_1
} else if fs::metadata(&resolved_file_2).is_ok() {
debugging!("Load config from: {resolved_file_2}");
resolved_file_2
} else if fs::metadata(&resolved_file_3).is_ok() {
debugging!("Load config from: {resolved_file_3}");
resolved_file_3
} else {
warning!("Cannot find config file from:\n- {resolved_file_1}\n- {resolved_file_2}\n- {resolved_file_3}");
resolved_file_1
};
Self::load_file(&config_file)
}
pub fn load_file(file: &str) -> XResult<Self> {
let resolved_file = resolve_file_path(file); let resolved_file = resolve_file_path(file);
let config_contents = opt_result!( let config_content = opt_result!(
fs::read_to_string(&resolved_file), "Read file: {}, failed: {}", file fs::read_to_string(resolved_file),
"Read config file: {}, failed: {}",
file
); );
let mut config: TinyEncryptConfig = opt_result!( Self::load_content(&config_content, file)
serde_json::from_str(&config_contents),"Parse file: {}, failed: {}", file); }
let mut splitted_profiles = HashMap::new();
for (k, v) in config.profiles.into_iter() { pub fn load_content(config_content: &str, file: &str) -> XResult<Self> {
if !k.contains(",") { let config: TinyEncryptConfig = opt_result!(
splitted_profiles.insert(k, v); serde_json::from_str(&config_content),
} else { "Parse config file: {}, failed: {}",
k.split(",") file
.map(|k| k.trim()) );
.filter(|k| k.len() > 0) debugging!("Config: {:#?}", config);
.for_each(|k| { let config = load_includes_and_merge(config);
splitted_profiles.insert(k.to_string(), v.clone()); debugging!("Final config: {:#?}", config);
});
if let Some(environment) = &config.environment {
for (k, v) in environment {
let v = match v {
StringOrVecString::String(s) => s.to_string(),
StringOrVecString::Vec(vs) => vs.join(","),
};
debugging!("Set env: {}={}", k, v);
env::set_var(k, v);
} }
} }
config.profiles = splitted_profiles;
Ok(config) Ok(config)
} }
pub fn find_envelops(&self, profile: &Option<String>) -> XResult<Vec<&TinyEncryptConfigEnvelop>> { pub fn resolve_path_namespace(&self, path: &Path, append_te: bool) -> PathBuf {
let profile = profile.as_ref().map(String::as_str).unwrap_or("default"); if let Some(path_str) = path.to_str() {
debugging!("Profile: {}", profile); if path_str.starts_with(':') {
let mut matched_envelops_map = HashMap::new(); let namespace = path_str
if let Some(key_ids) = self.profiles.get(profile) { .chars()
if key_ids.is_empty() { .skip(1)
return simple_error!("Profile: {} contains no valid envelopes", profile); .take_while(|c| *c != ':')
} .collect::<String>();
for key_id in key_ids { let mut filename = path_str
self.envelops.iter().for_each(|envelop| { .chars()
let is_matched = (&envelop.kid == key_id) .skip(1)
|| key_id == &format!("type:{}", &envelop.r#type.get_name()); .skip_while(|c| *c != ':')
if is_matched { .skip(1)
matched_envelops_map.insert(&envelop.kid, envelop); .collect::<String>();
} if append_te && !filename.ends_with(TINY_ENC_FILE_EXT) {
}); filename.push_str(TINY_ENC_FILE_EXT);
}
match self.find_namespace(&namespace) {
None => warning!("Namespace: {} not found", &namespace),
Some(dir) => return PathBuf::from(dir).join(&filename),
}
} }
} }
let mut envelops: Vec<_> = matched_envelops_map.values() path.to_path_buf()
.map(|envelop| *envelop) }
.collect();
pub fn find_namespace(&self, prefix: &str) -> Option<&String> {
self.namespaces.as_ref().and_then(|m| m.get(prefix))
}
pub fn find_first_arg_by_kid(&self, kid: &str) -> Option<&String> {
self.find_args_by_kid(kid).and_then(|a| a.iter().next())
}
pub fn find_args_by_kid(&self, kid: &str) -> Option<&Vec<String>> {
self.find_by_kid(kid).and_then(|e| e.args.as_ref())
}
pub fn find_by_kid(&self, kid: &str) -> Option<&TinyEncryptConfigEnvelop> {
self.find_by_kid_or_filter(kid, |_| false).first().copied()
}
pub fn find_by_kid_or_type(&self, k_filter: &str) -> Vec<&TinyEncryptConfigEnvelop> {
self.find_by_kid_or_filter(k_filter, |e| {
let envelop_type = format!("type:{}", &e.r#type.get_name());
if k_filter == "ALL" || k_filter == "*" || k_filter == envelop_type {
return true;
}
if k_filter.ends_with('*') {
let new_k_filter = k_filter.chars().collect::<Vec<_>>();
let new_k_filter = new_k_filter
.iter()
.take(new_k_filter.len() - 1)
.collect::<String>();
if e.kid.starts_with(&new_k_filter) || envelop_type.starts_with(&new_k_filter) {
return true;
}
}
false
})
}
pub fn find_by_kid_or_filter<F>(&self, kid: &str, f: F) -> Vec<&TinyEncryptConfigEnvelop>
where
F: Fn(&TinyEncryptConfigEnvelop) -> bool,
{
self.envelops
.iter()
.filter(|e| {
if e.kid == kid {
return true;
}
if let Some(sid) = &e.sid {
if sid == kid {
return true;
}
}
f(e)
})
.collect()
}
pub fn find_envelops(
&self,
profile: &Option<String>,
key_filter: &Option<String>,
) -> XResult<Vec<&TinyEncryptConfigEnvelop>> {
debugging!("Profile: {:?}", profile);
debugging!("Key filter: {:?}", key_filter);
let mut matched_envelops_map = HashMap::new();
let mut key_ids = vec![];
if key_filter.is_none() || profile.is_some() {
let profile = profile.as_ref().map(String::as_str).unwrap_or("default");
if profile == "ALL" {
self.envelops.iter().for_each(|e| {
key_ids.push(e.kid.to_string());
});
} else if let Some(kids) = self.get_profile(profile) {
kids.iter().for_each(|k| key_ids.push(k.to_string()));
}
}
if let Some(key_filter) = key_filter {
key_filter.split(',').for_each(|k| {
let k = k.trim();
if !k.is_empty() {
key_ids.push(k.to_string());
}
});
}
if key_ids.is_empty() {
// return simple_error!("Profile or key filter cannot find any valid envelopes");
return Ok(vec![]);
}
for key_id in &key_ids {
for envelop in self.find_by_kid_or_type(key_id) {
matched_envelops_map.insert(&envelop.kid, envelop);
}
}
let mut envelops: Vec<_> = matched_envelops_map.values().copied().collect();
if envelops.is_empty() { if envelops.is_empty() {
return simple_error!("Profile: {} has no valid envelopes found", profile); // return simple_error!("Profile or key filter cannot find any valid envelopes");
return Ok(vec![]);
} }
envelops.sort_by(|e1, e2| { envelops.sort_by(|e1, e2| {
if e1.r#type < e2.r#type { return Ordering::Greater; } if e1.r#type < e2.r#type {
if e1.r#type > e2.r#type { return Ordering::Less; } return Ordering::Greater;
if e1.kid < e2.kid { return Ordering::Greater; } }
if e1.kid > e2.kid { return Ordering::Less; } if e1.r#type > e2.r#type {
return Ordering::Less;
}
if e1.kid < e2.kid {
return Ordering::Greater;
}
if e1.kid > e2.kid {
return Ordering::Less;
}
Ordering::Equal Ordering::Equal
}); });
Ok(envelops) Ok(envelops)
} }
} }
pub fn resolve_path_namespace(
config: &Option<TinyEncryptConfig>,
path: &Path,
append_te: bool,
) -> PathBuf {
match config {
None => path.to_path_buf(),
Some(config) => config.resolve_path_namespace(path, append_te),
}
}
pub fn load_includes_and_merge(mut config: TinyEncryptConfig) -> TinyEncryptConfig {
debugging!("Config includes: {:?}", &config.includes);
if let Some(includes) = &config.includes {
let sub_configs = search_include_configs(includes);
debugging!(
"Found {} sub configs, detail {:?}",
sub_configs.len(),
sub_configs
);
for sub_config in &sub_configs {
// merge environment
if let Some(sub_environment) = &sub_config.environment {
match &mut config.environment {
None => {
config.environment = Some(sub_environment.clone());
}
Some(env) => {
for (k, v) in sub_environment {
match env.get_mut(k) {
None => {
env.insert(k.clone(), v.clone());
}
Some(env_val) => {
match (env_val, v) {
(StringOrVecString::Vec(env_value_vec), StringOrVecString::Vec(v_vec)) => {
for vv in v_vec {
if !env_value_vec.contains(vv) {
env_value_vec.push(vv.clone());
}
}
}
_ => {
warning!("Duplicate or mis-match environment value, key: {}", k);
}
}
}
}
}
}
}
}
// merge profiles
for sub_envelop in &sub_config.envelops {
let filter_envelops = config.envelops.iter().filter(|e| {
e.kid == sub_envelop.kid || (e.sid.is_some() && e.sid == sub_envelop.sid)
}).collect::<Vec<_>>();
if !filter_envelops.is_empty() {
warning!("Duplication kid: {} or sid: {:?}", sub_envelop.kid, sub_envelop.sid);
continue;
}
config.envelops.push(sub_envelop.clone());
}
// deal with envelop profiles
let mut sub_profiles: HashMap<String, Vec<String>> = match &sub_config.profiles {
None => HashMap::new(),
Some(sub_profiles) => sub_profiles.clone(),
};
for envelop in &sub_config.envelops {
if let Some(profiles) = &envelop.profiles {
let kid = envelop.kid.clone();
for profile in profiles {
match sub_profiles.get_mut(profile) {
None => {
sub_profiles.insert(profile.clone(), vec![kid.clone()]);
}
Some(kids) => {
if !kids.contains(&kid) {
kids.push(kid.clone());
}
}
}
}
}
}
// merge profiles
match &mut config.profiles {
None => {
config.profiles = Some(sub_profiles.clone());
}
Some(profiles) => {
for (k, v) in &sub_profiles {
match profiles.get_mut(k) {
None => {
profiles.insert(k.clone(), v.clone());
}
Some(env_val) => {
for vv in v {
env_val.push(vv.clone());
}
}
}
}
}
}
}
}
if let Some(profiles) = &mut config.profiles {
let all_key_ids = config.envelops.iter().map(|e| e.kid.clone()).collect::<Vec<_>>();
if profiles.contains_key("__all__") {
warning!("Key __all__ in profiles exists")
} else {
profiles.insert("__all__".to_string(), all_key_ids);
}
}
config
}
pub fn search_include_configs(includes_path: &str) -> Vec<TinyEncryptConfig> {
let includes_path = if includes_path.starts_with("$") {
let includes_path_env_var = includes_path.chars().skip(1).collect::<String>();
match rust_util_env::env_var(&includes_path_env_var) {
Some(includes_path) => includes_path,
None => {
warning!("Cannot find env var: {}", &includes_path_env_var);
return vec![];
}
}
} else {
includes_path.to_string()
};
let includes_path = &includes_path;
let mut sub_configs = vec![];
let read_dir = match fs::read_dir(includes_path) {
Ok(read_dir) => read_dir,
Err(e) => {
warning!("Read dir: {}, failed: {}", includes_path, e);
return sub_configs;
}
};
for entry in read_dir {
let entry = match entry {
Ok(entry) => entry,
Err(e) => {
warning!("Read dir: {} entry, failed: {}", includes_path, e);
continue;
}
};
let file_name = entry.file_name();
let file_name = file_name.to_str();
let file_name = match file_name {
Some(file_name) => file_name,
None => continue,
};
if file_name.ends_with(".tinyencrypt.json") {
debugging!("Matches config file: {}", file_name);
let file_path = entry.path();
let content = match fs::read_to_string(entry.path()) {
Ok(content) => content,
Err(e) => {
warning!("Read config file: {:?}, failed: {}", file_path, e);
continue;
}
};
let config = match serde_json::from_str::<TinyEncryptConfig>(&content) {
Ok(config) => config,
Err(e) => {
warning!("Parse config file: {:?}, failed: {}", file_path, e);
continue;
}
};
sub_configs.push(config);
}
}
sub_configs
}

36
src/consts.rs Normal file
View File

@@ -0,0 +1,36 @@
// AES-GCM-ECDH Algorithms
pub const ENC_AES256_GCM_P256: &str = "aes256-gcm-p256";
pub const ENC_AES256_GCM_P384: &str = "aes256-gcm-p384";
pub const ENC_AES256_GCM_X25519: &str = "aes256-gcm-x25519";
pub const ENC_AES256_GCM_KYBER1204: &str = "aes256-gcm-kyber1204";
pub const ENC_AES256_GCM_MLKEM768: &str = "aes256-gcm-mlkem768";
pub const ENC_AES256_GCM_MLKEM1024: &str = "aes256-gcm-mlkem1024";
pub const ENC_CHACHA20_POLY1305_P256: &str = "chacha20-poly1305-p256";
pub const ENC_CHACHA20_POLY1305_P384: &str = "chacha20-poly1305-p384";
pub const ENC_CHACHA20_POLY1305_X25519: &str = "chacha20-poly1305-x25519";
pub const ENC_CHACHA20_POLY1305_KYBER1204: &str = "chacha20-poly1305-kyber1204";
pub const ENC_CHACHA20_POLY1305_MLKEM768: &str = "chacha20-poly1305-mlkem768";
pub const ENC_CHACHA20_POLY1305_MLKEM1024: &str = "chacha20-poly1305-mlkem1024";
// Extend and config file
pub const TINY_ENC_FILE_EXT: &str = ".tinyenc";
pub const TINY_ENC_PEM_FILE_EXT: &str = ".tinyenc.pem";
pub const ENV_TINY_ENC_CONFIG_FILE: &str = "TINY_ENCRYPT_CONFIG_FILE";
pub const TINY_ENC_CONFIG_FILE: &str = "~/.tinyencrypt/config-rs.json";
pub const TINY_ENC_CONFIG_FILE_2: &str = "~/.config/tinyencrypt-rs.json";
pub const TINY_ENC_CONFIG_FILE_3: &str = "/etc/tinyencrypt/config-rs.json";
pub const TINY_ENC_PEM_NAME: &str = "TINY ENCRYPT";
pub const TINY_ENC_AES_GCM: &str = "AES/GCM";
pub const TINY_ENC_CHACHA20_POLY1305: &str = "CHACHA20/POLY1305";
// Tiny enc magic tag
pub const TINY_ENC_MAGIC_TAG: u16 = 0x01;
pub const TINY_ENC_COMPRESSED_MAGIC_TAG: u16 = 0x02;
// Encryption nonce salt
pub const SALT_COMMENT: &[u8] = b"salt:comment";
pub const SALT_META: &[u8] = b"salt:meta";
pub const DATE_TIME_FORMAT: &str = "EEE MMM dd HH:mm:ss z yyyy";

View File

@@ -1,79 +0,0 @@
use aes_gcm_stream::{Aes256GcmStreamDecryptor, Aes256GcmStreamEncryptor};
use rust_util::{opt_result, XResult};
pub fn aes_gcm_decrypt(key: &[u8], nonce: &[u8], message: &[u8]) -> XResult<Vec<u8>> {
let key: [u8; 32] = opt_result!(key.try_into(), "Invalid envelop: {}");
let mut aes256_gcm = Aes256GcmStreamDecryptor::new(key, nonce);
let mut b1 = aes256_gcm.update(message);
let b2 = opt_result!(aes256_gcm.finalize(), "Invalid envelop: {}");
b1.extend_from_slice(&b2);
Ok(b1)
}
pub fn aes_gcm_encrypt(key: &[u8], nonce: &[u8], message: &[u8]) -> XResult<Vec<u8>> {
let key: [u8; 32] = opt_result!(key.try_into(), "Invalid envelop: {}");
let mut aes256_gcm = Aes256GcmStreamEncryptor::new(key, nonce);
let mut b1 = aes256_gcm.update(message);
let (b2, tag) = aes256_gcm.finalize();
b1.extend_from_slice(&b2);
b1.extend_from_slice(&tag);
Ok(b1)
}
#[test]
fn test_aes_gcm_01() {
let data_key = hex::decode("0001020304050607080910111213141516171819202122232425262728293031").unwrap();
let nonce = hex::decode("000102030405060708091011").unwrap();
let plain_text1 = "Hello world!".as_bytes();
let encrypted_text1 = "dce9511866417cff5123fa08c9e92cf156c5fc8bf6108ff28816fb58";
let plain_text2 = "This is a test message.".as_bytes();
let encrypted_text2 = "c0e45407290878b0426fea4c09597ce323b056f975c63cce6c8da516c2a78c7d71b590c869cf92";
let key256: [u8; 32] = data_key.as_slice().try_into().unwrap();
{
let mut encryptor = Aes256GcmStreamEncryptor::new(key256.clone(), &nonce);
let mut encrypted = encryptor.update(plain_text1);
let (last_block, tag) = encryptor.finalize();
encrypted.extend_from_slice(&last_block);
encrypted.extend_from_slice(&tag);
assert_eq!(encrypted_text1, hex::encode(&encrypted));
}
{
let mut encryptor = Aes256GcmStreamEncryptor::new(key256.clone(), &nonce);
let mut encrypted = encryptor.update(plain_text2);
let (last_block, tag) = encryptor.finalize();
encrypted.extend_from_slice(&last_block);
encrypted.extend_from_slice(&tag);
assert_eq!(encrypted_text2, hex::encode(&encrypted));
}
}
#[test]
fn test_aes_gcm_02() {
let data_key = hex::decode("aa01020304050607080910111213141516171819202122232425262728293031").unwrap();
let nonce = hex::decode("aa0102030405060708091011").unwrap();
let plain_text1 = hex::encode("Hello world!".as_bytes());
let encrypted_text1 = hex::decode("42b625d2bacb8a514076f14002f02770e9ccd98c90e556dc267aca30").unwrap();
let plain_text2 = hex::encode("This is a test message.".as_bytes());
let encrypted_text2 = hex::decode("5ebb20cdf5828e1e533ae1043ce6703cfa51574a83a069700aedefdbe2c735b01b74da214cba4a").unwrap();
let key256: [u8; 32] = data_key.as_slice().try_into().unwrap();
{
let mut decryptor = Aes256GcmStreamDecryptor::new(key256.clone(), &nonce);
let mut plain_text = decryptor.update(encrypted_text1.as_slice());
let last_block = decryptor.finalize().unwrap();
plain_text.extend_from_slice(&last_block);
assert_eq!(plain_text1, hex::encode(&plain_text));
}
{
let mut decryptor = Aes256GcmStreamDecryptor::new(key256.clone(), &nonce);
let mut plain_text = decryptor.update(encrypted_text2.as_slice());
let last_block = decryptor.finalize().unwrap();
plain_text.extend_from_slice(&last_block);
assert_eq!(plain_text2, hex::encode(&plain_text));
}
}

190
src/crypto_cryptor.rs Normal file
View File

@@ -0,0 +1,190 @@
use aes_gcm_stream::{Aes256GcmStreamDecryptor, Aes256GcmStreamEncryptor};
use chacha20_poly1305_stream::{ChaCha20Poly1305StreamDecryptor, ChaCha20Poly1305StreamEncryptor};
use rust_util::{opt_result, simple_error, XResult};
use zeroize::Zeroize;
use crate::{consts, util_env};
pub struct KeyNonce<'a, 'b> {
pub k: &'a [u8],
pub n: &'b [u8],
}
#[derive(Debug, Copy, Clone)]
pub enum Cryptor {
Aes256Gcm,
ChaCha20Poly1305,
}
impl Cryptor {
pub fn from(algorithm: &str) -> XResult<Self> {
match algorithm {
"aes256-gcm" | consts::TINY_ENC_AES_GCM => Ok(Cryptor::Aes256Gcm),
"chacha20-poly1305" | consts::TINY_ENC_CHACHA20_POLY1305 => Ok(Cryptor::ChaCha20Poly1305),
_ => simple_error!("Unknown algorithm: {}",algorithm),
}
}
pub fn get_name(&self) -> String {
let name = match self {
Cryptor::Aes256Gcm => consts::TINY_ENC_AES_GCM,
Cryptor::ChaCha20Poly1305 => consts::TINY_ENC_CHACHA20_POLY1305,
};
name.to_string()
}
pub fn encryptor(self, key_nonce: &KeyNonce) -> XResult<Box<dyn Encryptor>> {
get_encryptor(self, key_nonce)
}
pub fn decryptor(self, key_nonce: &KeyNonce) -> XResult<Box<dyn Decryptor>> {
get_decryptor(self, key_nonce)
}
}
pub trait Encryptor {
fn update(&mut self, message: &[u8]) -> Vec<u8>;
fn finalize(&mut self) -> (Vec<u8>, Vec<u8>);
fn encrypt(&mut self, message: &[u8]) -> Vec<u8> {
let mut cipher_text = self.update(message);
let (last_block, tag) = self.finalize();
cipher_text.extend_from_slice(&last_block);
cipher_text.extend_from_slice(&tag);
cipher_text
}
}
pub trait Decryptor {
fn update(&mut self, message: &[u8]) -> Vec<u8>;
fn finalize(&mut self) -> XResult<Vec<u8>>;
fn decrypt(&mut self, message: &[u8]) -> XResult<Vec<u8>> {
let mut plaintext = self.update(message);
let last_block = self.finalize()?;
plaintext.extend_from_slice(&last_block);
Ok(plaintext)
}
}
fn get_encryptor(crypto: Cryptor, key_nonce: &KeyNonce) -> XResult<Box<dyn Encryptor>> {
match crypto {
Cryptor::Aes256Gcm => {
let mut key: [u8; 32] = opt_result!(key_nonce.k.try_into(), "Bad AES 256 key: {}");
let aes256_gcm_stream_encryptor = Aes256GcmStreamEncryptor::new(
key, key_nonce.n);
key.zeroize();
Ok(Box::new(Aes256GcmEncryptor {
aes256_gcm_stream_encryptor,
}))
}
Cryptor::ChaCha20Poly1305 => Ok(Box::new(ChaCha20Poly1305Encryptor {
chacha20_poly1305_stream_encryptor: ChaCha20Poly1305StreamEncryptor::new(
key_nonce.k, key_nonce.n)?,
}))
}
}
fn get_decryptor(crypto: Cryptor, key_nonce: &KeyNonce) -> XResult<Box<dyn Decryptor>> {
match crypto {
Cryptor::Aes256Gcm => {
let mut key: [u8; 32] = opt_result!(key_nonce.k.try_into(), "Bad AES 256 key: {}");
let aes256_gcm_stream_decryptor = Aes256GcmStreamDecryptor::new(
key, key_nonce.n);
key.zeroize();
Ok(Box::new(Aes256GcmDecryptor {
aes256_gcm_stream_decryptor,
}))
}
Cryptor::ChaCha20Poly1305 => Ok(Box::new(ChaCha20Poly1305Decryptor {
chacha20_poly1305_stream_decryptor: ChaCha20Poly1305StreamDecryptor::new(
key_nonce.k, key_nonce.n)?,
}))
}
}
pub struct Aes256GcmEncryptor {
aes256_gcm_stream_encryptor: Aes256GcmStreamEncryptor,
}
impl Encryptor for Aes256GcmEncryptor {
fn update(&mut self, message: &[u8]) -> Vec<u8> {
self.aes256_gcm_stream_encryptor.update(message)
}
fn finalize(&mut self) -> (Vec<u8>, Vec<u8>) {
self.aes256_gcm_stream_encryptor.finalize()
}
}
pub struct ChaCha20Poly1305Encryptor {
chacha20_poly1305_stream_encryptor: ChaCha20Poly1305StreamEncryptor,
}
impl Encryptor for ChaCha20Poly1305Encryptor {
fn update(&mut self, message: &[u8]) -> Vec<u8> {
self.chacha20_poly1305_stream_encryptor.update(message)
}
fn finalize(&mut self) -> (Vec<u8>, Vec<u8>) {
self.chacha20_poly1305_stream_encryptor.finalize()
}
}
pub struct Aes256GcmDecryptor {
aes256_gcm_stream_decryptor: Aes256GcmStreamDecryptor,
}
impl Decryptor for Aes256GcmDecryptor {
fn update(&mut self, message: &[u8]) -> Vec<u8> {
self.aes256_gcm_stream_decryptor.update(message)
}
fn finalize(&mut self) -> XResult<Vec<u8>> {
Ok(self.aes256_gcm_stream_decryptor.finalize()?)
}
}
pub struct ChaCha20Poly1305Decryptor {
chacha20_poly1305_stream_decryptor: ChaCha20Poly1305StreamDecryptor,
}
impl Decryptor for ChaCha20Poly1305Decryptor {
fn update(&mut self, message: &[u8]) -> Vec<u8> {
self.chacha20_poly1305_stream_decryptor.update(message)
}
fn finalize(&mut self) -> XResult<Vec<u8>> {
Ok(self.chacha20_poly1305_stream_decryptor.finalize()?)
}
}
#[allow(clippy::redundant_closure)]
pub fn get_cryptor_by_encryption_algorithm(encryption_algorithm: &Option<String>) -> XResult<Cryptor> {
let encryption_algorithm = encryption_algorithm.as_deref()
.or_else(|| util_env::get_default_encryption_algorithm())
.unwrap_or(consts::TINY_ENC_AES_GCM)
.to_lowercase();
let cryptor = match encryption_algorithm.as_str() {
"aes" | "aes/gcm" => Cryptor::Aes256Gcm,
"chacha20" | "chacha20/poly1305" => Cryptor::ChaCha20Poly1305,
_ => return simple_error!("Unknown encryption algorithm: {}, should be AES or CHACHA20", encryption_algorithm),
};
Ok(cryptor)
}
#[test]
fn test_cryptor() {
let key = [0u8; 32];
let nonce = [0u8; 12];
let key_nonce = KeyNonce { k: &key, n: &nonce };
let ciphertext = Cryptor::Aes256Gcm.encryptor(&key_nonce).unwrap()
.encrypt(b"hello world");
let plaintext = Cryptor::Aes256Gcm.decryptor(&key_nonce).unwrap()
.decrypt(&ciphertext).unwrap();
assert_eq!(b"hello world", plaintext.as_slice());
}

35
src/crypto_simple.rs Normal file
View File

@@ -0,0 +1,35 @@
use rust_util::XResult;
use crate::crypto_cryptor::{Cryptor, KeyNonce};
use crate::util_digest;
pub fn try_decrypt_with_salt(crypto: Cryptor, key_nonce: &KeyNonce, salt: &[u8], message: &[u8]) -> XResult<Vec<u8>> {
let new_nonce = build_salted_nonce(key_nonce.n, salt);
let new_key_nonce = KeyNonce { k: key_nonce.k, n: &new_nonce };
if let Ok(decrypted) = decrypt(crypto, &new_key_nonce, message) {
return Ok(decrypted);
}
decrypt(crypto, key_nonce, message)
}
pub fn decrypt(crypto: Cryptor, key_nonce: &KeyNonce, message: &[u8]) -> XResult<Vec<u8>> {
crypto.decryptor(key_nonce)?.decrypt(message)
}
pub fn encrypt_with_salt(crypto: Cryptor, key_nonce: &KeyNonce, salt: &[u8], message: &[u8]) -> XResult<Vec<u8>> {
let new_nonce = build_salted_nonce(key_nonce.n, salt);
let new_key_nonce = KeyNonce { k: key_nonce.k, n: &new_nonce };
encrypt(crypto, &new_key_nonce, message)
}
pub fn encrypt(crypto: Cryptor, key_nonce: &KeyNonce, message: &[u8]) -> XResult<Vec<u8>> {
Ok(crypto.encryptor(key_nonce)?.encrypt(message))
}
fn build_salted_nonce(nonce: &[u8], salt: &[u8]) -> Vec<u8> {
let mut nonce_with_salt = nonce.to_vec();
nonce_with_salt.extend_from_slice(salt);
let input = util_digest::sha256_digest(&nonce_with_salt);
// let input = hex::decode(sha256::digest(nonce_with_salt)).unwrap();
input[0..12].to_vec()
}

View File

@@ -1,45 +0,0 @@
use std::io::{Read, Write};
use rust_util::{opt_result, simple_error, XResult};
use crate::spec::TinyEncryptMeta;
use crate::util;
pub fn _write_tiny_encrypt_meta<W: Write>(w: &mut W, meta: &TinyEncryptMeta) -> XResult<usize> {
let meta_json = opt_result!( serde_json::to_string(meta), "Meta to JSON failed: {}");
let meta_json_bytes = meta_json.as_bytes();
let meta_json_bytes_len = meta_json_bytes.len();
opt_result!(w.write_all(&((0x01) as u16).to_be_bytes()), "Write tag failed: {}");
opt_result!(w.write_all(&(meta_json_bytes_len as u32).to_be_bytes()), "Write length failed: {}");
opt_result!(w.write_all(&meta_json_bytes), "Write meta failed: {}");
Ok(meta_json_bytes_len + 2 + 4)
}
pub fn read_tiny_encrypt_meta_and_normalize<R: Read>(r: &mut R) -> XResult<TinyEncryptMeta> {
let mut meta = read_tiny_encrypt_meta(r);
let _ = meta.as_mut().map(|meta| meta.normalize());
meta
}
pub fn read_tiny_encrypt_meta<R: Read>(r: &mut R) -> XResult<TinyEncryptMeta> {
let mut tag_buff = [0_u8; 2];
opt_result!(r.read_exact(&mut tag_buff), "Read tag failed: {}");
let tag = u16::from_be_bytes(tag_buff);
if tag != util::TINY_ENC_MAGIC_TAG {
return simple_error!("Tag is not 0x01, but is: 0x{:x}", tag);
}
let mut length_buff = [0_u8; 4];
opt_result!(r.read_exact(&mut length_buff), "Read length failed: {}");
let length = u32::from_be_bytes(length_buff);
if length > 1024 * 1024 {
return simple_error!("Meta too large: {}", length);
}
let mut meta_buff = vec![0; length as usize];
opt_result!(r.read_exact(meta_buff.as_mut_slice()), "Read meta failed: {}");
Ok(opt_result!(serde_json::from_slice(&meta_buff), "Parse meta failed: {}"))
}

82
src/lib.rs Normal file
View File

@@ -0,0 +1,82 @@
pub use cmd_config::CmdConfig;
pub use cmd_config::config;
#[cfg(feature = "decrypt")]
pub use cmd_decrypt::CmdDecrypt;
#[cfg(feature = "decrypt")]
pub use cmd_decrypt::decrypt;
#[cfg(feature = "decrypt")]
pub use cmd_decrypt::decrypt_single;
pub use cmd_directdecrypt::CmdDirectDecrypt;
pub use cmd_directdecrypt::direct_decrypt;
pub use cmd_encrypt::CmdEncrypt;
pub use cmd_simple_encrypt_decrypt::CmdSimpleEncrypt;
pub use cmd_simple_encrypt_decrypt::CmdSimpleDecrypt;
pub use cmd_encrypt::encrypt;
pub use cmd_encrypt::encrypt_single;
pub use cmd_encrypt::encrypt_single_file_out;
pub use cmd_simple_encrypt_decrypt::simple_encrypt;
#[cfg(feature = "decrypt")]
pub use cmd_simple_encrypt_decrypt::simple_decrypt;
#[cfg(feature = "decrypt")]
pub use cmd_execenv::CmdExecEnv;
#[cfg(feature = "decrypt")]
pub use cmd_execenv::exec_env;
pub use cmd_info::CmdInfo;
pub use cmd_info::info;
pub use cmd_info::info_single;
#[cfg(feature = "macos")]
pub use cmd_initkeychain::CmdInitKeychain;
#[cfg(feature = "macos")]
pub use cmd_initkeychain::init_keychain;
#[cfg(feature = "smartcard")]
pub use cmd_initpiv::CmdInitPiv;
#[cfg(feature = "smartcard")]
pub use cmd_initpiv::init_piv;
pub use cmd_version::CmdVersion;
pub use cmd_version::version;
pub use config::TinyEncryptConfig;
pub use util_log::init_tiny_encrypt_log;
mod consts;
mod util;
mod util_env;
mod util_digest;
mod util_progress;
#[cfg(feature = "smartcard")]
mod util_piv;
#[cfg(feature = "smartcard")]
mod util_pgp;
mod util_gpg;
mod util_ecdh;
mod compress;
mod config;
mod spec;
mod crypto_simple;
mod util_rsa;
mod crypto_cryptor;
mod wrap_key;
mod util_envelop;
mod util_file;
mod util_enc_file;
mod cmd_version;
mod cmd_config;
mod cmd_info;
#[cfg(feature = "decrypt")]
mod cmd_decrypt;
mod cmd_encrypt;
mod cmd_simple_encrypt_decrypt;
mod cmd_directdecrypt;
#[cfg(feature = "macos")]
mod cmd_initkeychain;
#[cfg(feature = "smartcard")]
mod cmd_initpiv;
#[cfg(feature = "macos")]
mod util_keychainstatic;
#[cfg(feature = "decrypt")]
mod cmd_execenv;
mod util_keychainkey;
mod util_simple_pbe;
mod util_log;
mod temporary_key;
mod util_mlkem;

View File

@@ -3,24 +3,20 @@ extern crate core;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use rust_util::XResult; use rust_util::XResult;
use crate::cmd_decrypt::CmdDecrypt; #[cfg(feature = "decrypt")]
use crate::cmd_encrypt::CmdEncrypt; use tiny_encrypt::CmdDecrypt;
use crate::cmd_info::CmdInfo; #[cfg(feature = "decrypt")]
use tiny_encrypt::CmdExecEnv;
#[cfg(feature = "macos")]
use tiny_encrypt::CmdInitKeychain;
#[cfg(feature = "smartcard")]
use tiny_encrypt::CmdInitPiv;
use tiny_encrypt::{init_tiny_encrypt_log, CmdConfig, CmdDirectDecrypt, CmdEncrypt, CmdInfo, CmdSimpleDecrypt, CmdSimpleEncrypt, CmdVersion};
mod util; use zeroizing_alloc::ZeroAlloc;
mod util_ecdh;
mod util_x25519; #[global_allocator]
mod compress; static ALLOC: ZeroAlloc<std::alloc::System> = ZeroAlloc(std::alloc::System);
mod config;
mod spec;
mod crypto_aes;
mod crypto_rsa;
mod wrap_key;
mod file;
mod card;
mod cmd_info;
mod cmd_decrypt;
mod cmd_encrypt;
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
#[command(name = "tiny-encrypt-rs")] #[command(name = "tiny-encrypt-rs")]
@@ -35,19 +31,63 @@ enum Commands {
/// Encrypt file(s) /// Encrypt file(s)
#[command(arg_required_else_help = true, short_flag = 'e')] #[command(arg_required_else_help = true, short_flag = 'e')]
Encrypt(CmdEncrypt), Encrypt(CmdEncrypt),
/// Simple encrypt message
#[command(arg_required_else_help = true, short_flag = 'E')]
SimpleEncrypt(CmdSimpleEncrypt),
#[cfg(feature = "decrypt")]
/// Simple decrypt message
#[command(arg_required_else_help = true, short_flag = 'D')]
SimpleDecrypt(CmdSimpleDecrypt),
#[cfg(feature = "decrypt")]
/// Decrypt file(s) /// Decrypt file(s)
#[command(arg_required_else_help = true, short_flag = 'd')] #[command(arg_required_else_help = true, short_flag = 'd')]
Decrypt(CmdDecrypt), Decrypt(CmdDecrypt),
/// Show file info /// Direct decrypt file(s)
#[command(arg_required_else_help = true)]
DirectDecrypt(CmdDirectDecrypt),
/// Show tiny encrypt file info
#[command(arg_required_else_help = true, short_flag = 'I')] #[command(arg_required_else_help = true, short_flag = 'I')]
Info(CmdInfo), Info(CmdInfo),
#[cfg(feature = "macos")]
/// Init Keychain (Secure Enclave or Static)
#[command(arg_required_else_help = true, short_flag = 'K')]
InitKeychain(CmdInitKeychain),
#[cfg(feature = "smartcard")]
/// Init PIV
#[command(arg_required_else_help = true, short_flag = 'P')]
InitPiv(CmdInitPiv),
#[cfg(feature = "decrypt")]
/// Execute environment
#[command(arg_required_else_help = true, short_flag = 'X')]
ExecEnv(CmdExecEnv),
/// Show version
#[command(short_flag = 'v')]
Version(CmdVersion),
/// Show configuration
#[command(short_flag = 'c')]
Config(CmdConfig),
} }
fn main() -> XResult<()> { fn main() -> XResult<()> {
init_tiny_encrypt_log();
let args = Cli::parse(); let args = Cli::parse();
match args.command { match args.command {
Commands::Encrypt(cmd_encrypt) => cmd_encrypt::encrypt(cmd_encrypt), Commands::Encrypt(cmd_encrypt) => tiny_encrypt::encrypt(cmd_encrypt),
Commands::Decrypt(cmd_decrypt) => cmd_decrypt::decrypt(cmd_decrypt), Commands::SimpleEncrypt(cmd_simple_encrypt) => tiny_encrypt::simple_encrypt(cmd_simple_encrypt),
Commands::Info(cmd_info) => cmd_info::info(cmd_info), #[cfg(feature = "decrypt")]
Commands::SimpleDecrypt(cmd_simple_decrypt) => tiny_encrypt::simple_decrypt(cmd_simple_decrypt),
#[cfg(feature = "decrypt")]
Commands::Decrypt(cmd_decrypt) => tiny_encrypt::decrypt(cmd_decrypt),
Commands::DirectDecrypt(cmd_direct_decrypt) => tiny_encrypt::direct_decrypt(cmd_direct_decrypt),
Commands::Info(cmd_info) => tiny_encrypt::info(cmd_info),
#[cfg(feature = "macos")]
Commands::InitKeychain(cmd_keychain_key) => tiny_encrypt::init_keychain(cmd_keychain_key),
#[cfg(feature = "smartcard")]
Commands::InitPiv(cmd_init_piv) => tiny_encrypt::init_piv(cmd_init_piv),
#[cfg(feature = "decrypt")]
Commands::ExecEnv(cmd_exec_env) => tiny_encrypt::exec_env(cmd_exec_env),
Commands::Version(cmd_version) => tiny_encrypt::version(cmd_version),
Commands::Config(cmd_config) => tiny_encrypt::config(cmd_config),
} }
} }

View File

@@ -1,38 +1,57 @@
use std::fs::Metadata; use std::fs::Metadata;
use rust_util::util_time; use rust_util::{opt_result, util_time, XResult};
use rust_util::util_time::get_millis; use rust_util::util_time::get_millis;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::util::{encode_base64, get_user_agent, TINY_ENC_AES_GCM}; use crate::{compress, crypto_simple};
use crate::consts::SALT_META;
use crate::crypto_cryptor::{Cryptor, KeyNonce};
use crate::util::{encode_base64, get_user_agent};
pub const TINY_ENCRYPT_VERSION_10: &'static str = "1.0"; // Compatible with 1.0 is removed from v0.6.0
pub const TINY_ENCRYPT_VERSION_11: &'static str = "1.1"; // pub const TINY_ENCRYPT_VERSION_10: &str = "1.0";
pub const TINY_ENCRYPT_VERSION_11: &str = "1.1";
/// Specification: [Tiny Encrypt Spec V1.1](https://git.hatter.ink/hatter/tiny-encrypt-java/src/branch/master/TinyEncryptSpecV1.1.md) /// Specification: [Tiny Encrypt Spec V1.1](https://github.com/OpenWebStandard/tiny-encrypt-format-spec/blob/main/TinyEncryptSpecv1.1.md)
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct TinyEncryptMeta { pub struct TinyEncryptMeta {
pub version: String, pub version: String,
pub created: u64, pub created: u64,
pub user_agent: String, pub user_agent: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub latest_user_agent: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub comment: Option<String>, pub comment: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub encrypted_comment: Option<String>, pub encrypted_comment: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub encrypted_meta: Option<String>, pub encrypted_meta: Option<String>,
// --------------------------------------- // ---------------------------------------
#[serde(skip_serializing_if = "Option::is_none")]
pub pgp_envelop: Option<String>, pub pgp_envelop: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pgp_fingerprint: Option<String>, pub pgp_fingerprint: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub age_envelop: Option<String>, pub age_envelop: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub age_recipient: Option<String>, pub age_recipient: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ecdh_envelop: Option<String>, pub ecdh_envelop: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ecdh_point: Option<String>, pub ecdh_point: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub envelop: Option<String>, pub envelop: Option<String>,
// --------------------------------------- // ---------------------------------------
pub envelops: Option<Vec<TinyEncryptEnvelop>>, pub envelops: Option<Vec<TinyEncryptEnvelop>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub encryption_algorithm: Option<String>, pub encryption_algorithm: Option<String>,
pub nonce: String, pub nonce: String,
pub file_length: u64, pub file_length: u64,
pub file_last_modified: u64, pub file_last_modified: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub file_edit_count: Option<u64>,
pub compress: bool, pub compress: bool,
} }
@@ -41,6 +60,7 @@ pub struct TinyEncryptMeta {
pub struct TinyEncryptEnvelop { pub struct TinyEncryptEnvelop {
pub r#type: TinyEncryptEnvelopType, pub r#type: TinyEncryptEnvelopType,
pub kid: String, pub kid: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub desc: Option<String>, pub desc: Option<String>,
pub encrypted_key: String, pub encrypted_key: String,
} }
@@ -48,14 +68,55 @@ pub struct TinyEncryptEnvelop {
/// NOTICE: Kms and Age is not being supported in the future /// NOTICE: Kms and Age is not being supported in the future
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, PartialOrd)] #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, PartialOrd)]
pub enum TinyEncryptEnvelopType { pub enum TinyEncryptEnvelopType {
#[serde(rename = "pgp")] // OpenPGP Card RSA
Pgp, #[serde(rename = "pgp-rsa", alias = "pgp")]
PgpRsa,
// OpenPGP Card X25519
#[serde(rename = "pgp-x25519")] #[serde(rename = "pgp-x25519")]
PgpX25519, PgpX25519,
// GPG Command Line
#[serde(rename = "gpg")]
Gpg,
// Keychain Static X25519 (less secure)
#[serde(rename = "static-x25519")]
StaticX25519,
// Keychain Static Kyber1024 (less secure)
#[serde(rename = "static-kyber1024")]
StaticKyber1024,
// Secure Enclave ECDH P256
#[serde(rename = "key-p256")]
KeyP256,
// Secure Enclave ML-KEM 768
#[serde(rename = "key-mlkem768")]
KeyMlKem768,
// Secure Enclave ML-KEM 1024
#[serde(rename = "key-mlkem1024")]
KeyMlKem1024,
// PIV ECDH P256
#[serde(rename = "piv-p256", alias = "ecdh")]
PivP256,
// PIV ECDH P384
#[serde(rename = "piv-p384", alias = "ecdh-p384")]
PivP384,
// External ECDH P256
#[serde(rename = "ext-p256")]
ExtP256,
// External ECDH P384
#[serde(rename = "ext-p384")]
ExtP384,
// External ML-KEM 768
#[serde(rename = "ext-mlkem768")]
ExtMlKem768,
// External ML-KEM 1024
#[serde(rename = "ext-mlkem1024")]
ExtMlKem1024,
// PIV RSA
#[serde(rename = "piv-rsa")]
PivRsa,
// Age, tiny-encrypt-rs is not supported
#[serde(rename = "age")] #[serde(rename = "age")]
Age, Age,
#[serde(rename = "ecdh")] // KMS, tiny-encrypt-rs is not supported
Ecdh,
#[serde(rename = "kms")] #[serde(rename = "kms")]
Kms, Kms,
} }
@@ -64,15 +125,126 @@ impl TinyEncryptEnvelopType {
pub fn get_upper_name(&self) -> String { pub fn get_upper_name(&self) -> String {
self.get_name().to_uppercase() self.get_name().to_uppercase()
} }
pub fn get_name(&self) -> &'static str { pub fn get_name(&self) -> &'static str {
match self { match self {
TinyEncryptEnvelopType::Pgp => "pgp", TinyEncryptEnvelopType::PgpRsa => "pgp-rsa",
TinyEncryptEnvelopType::PgpX25519 => "pgp-x25519", TinyEncryptEnvelopType::PgpX25519 => "pgp-x25519",
TinyEncryptEnvelopType::Gpg => "gpg",
TinyEncryptEnvelopType::StaticX25519 => "static-x25519",
TinyEncryptEnvelopType::StaticKyber1024 => "static-kyber1024",
TinyEncryptEnvelopType::KeyP256 => "key-p256",
TinyEncryptEnvelopType::KeyMlKem768 => "key-mlkem768",
TinyEncryptEnvelopType::KeyMlKem1024 => "key-mlkem1024",
TinyEncryptEnvelopType::ExtP256 => "ext-p256",
TinyEncryptEnvelopType::ExtP384 => "ext-p384",
TinyEncryptEnvelopType::ExtMlKem768 => "ext-mlkem768",
TinyEncryptEnvelopType::ExtMlKem1024 => "ext-mlkem1024",
TinyEncryptEnvelopType::PivP256 => "piv-p256",
TinyEncryptEnvelopType::PivP384 => "piv-p384",
TinyEncryptEnvelopType::PivRsa => "piv-rsa",
TinyEncryptEnvelopType::Age => "age", TinyEncryptEnvelopType::Age => "age",
TinyEncryptEnvelopType::Ecdh => "ecdh",
TinyEncryptEnvelopType::Kms => "kms", TinyEncryptEnvelopType::Kms => "kms",
} }
} }
pub fn from_name(name: &str) -> Option<Self> {
match name {
"pgp-rsa" => Some(TinyEncryptEnvelopType::PgpRsa),
"pgp-x25519" => Some(TinyEncryptEnvelopType::PgpX25519),
"gpg" => Some(TinyEncryptEnvelopType::Gpg),
"static-x25519" => Some(TinyEncryptEnvelopType::StaticX25519),
"static-kyber1024" => Some(TinyEncryptEnvelopType::StaticKyber1024),
"key-p256" => Some(TinyEncryptEnvelopType::KeyP256),
"key-mlkem768" => Some(TinyEncryptEnvelopType::KeyMlKem768),
"key-mlkem1024" => Some(TinyEncryptEnvelopType::KeyMlKem1024),
"ext-p256" => Some(TinyEncryptEnvelopType::ExtP256),
"ext-p384" => Some(TinyEncryptEnvelopType::ExtP384),
"ext-mlkem768" => Some(TinyEncryptEnvelopType::ExtMlKem768),
"ext-mlkem1024" => Some(TinyEncryptEnvelopType::ExtMlKem1024),
"piv-p256" => Some(TinyEncryptEnvelopType::PivP256),
"piv-p384" => Some(TinyEncryptEnvelopType::PivP384),
"piv-rsa" => Some(TinyEncryptEnvelopType::PivRsa),
"age" => Some(TinyEncryptEnvelopType::Age),
"kms" => Some(TinyEncryptEnvelopType::Kms),
_ => None,
}
}
pub fn auto_select(&self) -> bool {
match self {
TinyEncryptEnvelopType::StaticX25519
| TinyEncryptEnvelopType::StaticKyber1024
| TinyEncryptEnvelopType::KeyP256
| TinyEncryptEnvelopType::KeyMlKem768
| TinyEncryptEnvelopType::KeyMlKem1024
| TinyEncryptEnvelopType::Gpg
| TinyEncryptEnvelopType::Kms => true,
TinyEncryptEnvelopType::PgpRsa
| TinyEncryptEnvelopType::PgpX25519
| TinyEncryptEnvelopType::ExtP256
| TinyEncryptEnvelopType::ExtP384
| TinyEncryptEnvelopType::ExtMlKem768
| TinyEncryptEnvelopType::ExtMlKem1024
| TinyEncryptEnvelopType::PivP256
| TinyEncryptEnvelopType::PivP384
| TinyEncryptEnvelopType::PivRsa
| TinyEncryptEnvelopType::Age => false,
}
}
pub fn is_hardware_security(&self) -> Option<bool> {
match self {
TinyEncryptEnvelopType::PgpRsa
| TinyEncryptEnvelopType::PgpX25519
| TinyEncryptEnvelopType::KeyP256
| TinyEncryptEnvelopType::KeyMlKem768
| TinyEncryptEnvelopType::KeyMlKem1024
| TinyEncryptEnvelopType::PivP256
| TinyEncryptEnvelopType::PivP384
| TinyEncryptEnvelopType::PivRsa
| TinyEncryptEnvelopType::Age => Some(true),
TinyEncryptEnvelopType::StaticX25519
| TinyEncryptEnvelopType::StaticKyber1024
| TinyEncryptEnvelopType::Kms => Some(false),
// GPG is unknown(hardware/software)
TinyEncryptEnvelopType::Gpg
| TinyEncryptEnvelopType::ExtP256
| TinyEncryptEnvelopType::ExtP384
| TinyEncryptEnvelopType::ExtMlKem768
| TinyEncryptEnvelopType::ExtMlKem1024 => None,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct EncEncryptedMeta {
#[serde(skip_serializing_if = "Option::is_none")]
pub filename: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub c_time: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub m_time: Option<u64>,
}
impl EncEncryptedMeta {
pub fn unseal(crypto: Cryptor, key_nonce: &KeyNonce, message: &[u8]) -> XResult<Self> {
let mut decrypted = opt_result!(crypto_simple::try_decrypt_with_salt(
crypto, key_nonce, SALT_META, message), "Decrypt encrypted meta failed: {}");
decrypted = opt_result!(compress::decompress(&decrypted), "Depress encrypted meta failed: {}");
let meta = opt_result!(
serde_json::from_slice::<Self>(&decrypted), "Parse encrypted meta failed: {}");
Ok(meta)
}
pub fn seal(&self, crypto: Cryptor, key_nonce: &KeyNonce) -> XResult<Vec<u8>> {
let mut encrypted_meta_json = serde_json::to_vec(self).unwrap();
encrypted_meta_json = opt_result!(
compress::compress_default(&encrypted_meta_json), "Compress encrypted meta failed: {}");
let encrypted = opt_result!(crypto_simple::encrypt_with_salt(
crypto, key_nonce, SALT_META, encrypted_meta_json.as_slice()), "Encrypt encrypted meta failed: {}");
Ok(encrypted)
}
} }
pub struct EncMetadata { pub struct EncMetadata {
@@ -83,11 +255,12 @@ pub struct EncMetadata {
} }
impl TinyEncryptMeta { impl TinyEncryptMeta {
pub fn new(metadata: &Metadata, enc_metadata: &EncMetadata, nonce: &[u8], envelops: Vec<TinyEncryptEnvelop>) -> Self { pub fn new(metadata: &Metadata, enc_metadata: &EncMetadata, cryptor: Cryptor, nonce: &[u8], envelops: Vec<TinyEncryptEnvelop>) -> Self {
TinyEncryptMeta { TinyEncryptMeta {
version: TINY_ENCRYPT_VERSION_11.to_string(), version: TINY_ENCRYPT_VERSION_11.to_string(),
created: util_time::get_current_millis() as u64, created: util_time::get_current_millis() as u64,
user_agent: get_user_agent(), user_agent: get_user_agent(),
latest_user_agent: None,
comment: enc_metadata.comment.to_owned(), comment: enc_metadata.comment.to_owned(),
encrypted_comment: enc_metadata.encrypted_comment.to_owned(), encrypted_comment: enc_metadata.encrypted_comment.to_owned(),
encrypted_meta: enc_metadata.encrypted_meta.to_owned(), encrypted_meta: enc_metadata.encrypted_meta.to_owned(),
@@ -99,28 +272,30 @@ impl TinyEncryptMeta {
ecdh_point: None, ecdh_point: None,
envelop: None, envelop: None,
envelops: Some(envelops), envelops: Some(envelops),
encryption_algorithm: Some(TINY_ENC_AES_GCM.to_string()), encryption_algorithm: Some(cryptor.get_name()),
nonce: encode_base64(nonce), nonce: encode_base64(nonce),
file_length: metadata.len(), file_length: metadata.len(),
file_last_modified: match metadata.modified() { file_last_modified: match metadata.modified() {
Ok(modified) => get_millis(&modified) as u64, Ok(modified) => get_millis(&modified) as u64,
Err(_) => 0, Err(_) => 0,
}, },
file_edit_count: None,
compress: enc_metadata.compress, compress: enc_metadata.compress,
} }
} }
pub fn normalize(&mut self) { // compatibility with legacy tiny encrypt format
pub fn normalize_envelops(&mut self) {
if self.envelops.is_none() { if self.envelops.is_none() {
self.envelops = Some(vec![]); self.envelops = Some(vec![]);
} }
self.normalize_envelop(); self.normalize_kms_envelop();
self.normalize_pgp_envelop(); self.normalize_pgp_rsa_envelop();
self.normalize_age_envelop(); self.normalize_age_envelop();
self.normalize_ecdh_envelop(); self.normalize_piv_p256_envelop();
} }
fn normalize_envelop(&mut self) { fn normalize_kms_envelop(&mut self) {
if let (Some(envelop), Some(envelops)) = (&self.envelop, &mut self.envelops) { if let (Some(envelop), Some(envelops)) = (&self.envelop, &mut self.envelops) {
envelops.push(TinyEncryptEnvelop { envelops.push(TinyEncryptEnvelop {
r#type: TinyEncryptEnvelopType::Kms, r#type: TinyEncryptEnvelopType::Kms,
@@ -132,11 +307,11 @@ impl TinyEncryptMeta {
} }
} }
fn normalize_pgp_envelop(&mut self) { fn normalize_pgp_rsa_envelop(&mut self) {
if let (Some(pgp_envelop), Some(pgp_fingerprint), Some(envelops)) if let (Some(pgp_envelop), Some(pgp_fingerprint), Some(envelops))
= (&self.pgp_envelop, &self.pgp_fingerprint, &mut self.envelops) { = (&self.pgp_envelop, &self.pgp_fingerprint, &mut self.envelops) {
envelops.push(TinyEncryptEnvelop { envelops.push(TinyEncryptEnvelop {
r#type: TinyEncryptEnvelopType::Pgp, r#type: TinyEncryptEnvelopType::PgpRsa,
kid: pgp_fingerprint.into(), kid: pgp_fingerprint.into(),
desc: None, desc: None,
encrypted_key: pgp_envelop.into(), encrypted_key: pgp_envelop.into(),
@@ -160,11 +335,11 @@ impl TinyEncryptMeta {
} }
} }
fn normalize_ecdh_envelop(&mut self) { fn normalize_piv_p256_envelop(&mut self) {
if let (Some(ecdh_envelop), Some(ecdh_point), Some(envelops)) if let (Some(ecdh_envelop), Some(ecdh_point), Some(envelops))
= (&self.ecdh_envelop, &self.ecdh_point, &mut self.envelops) { = (&self.ecdh_envelop, &self.ecdh_point, &mut self.envelops) {
envelops.push(TinyEncryptEnvelop { envelops.push(TinyEncryptEnvelop {
r#type: TinyEncryptEnvelopType::Ecdh, r#type: TinyEncryptEnvelopType::PivP256,
kid: ecdh_point.into(), kid: ecdh_point.into(),
desc: None, desc: None,
encrypted_key: ecdh_envelop.into(), encrypted_key: ecdh_envelop.into(),

69
src/temporary_key.rs Normal file
View File

@@ -0,0 +1,69 @@
// syntax
// tiny-encrypt-key:type:sid:key_id:public_part[?key=value]
// e.g.
// tiny-encrypt-key:ext-p256:ext-key-1:02536aef5742b4288f1b44b3cc96f1556c35e4fac4e8e117e1f7ae091e42d0835b:04536aef5742b4288f1b44b3cc96f1556c35e4fac4e8e117e1f7ae091e42d0835bf3f95d932c22a74a91859bd7fdd8829a02d38cf4ec598b1cf6e02fa09f707a6f
use crate::config::TinyEncryptConfigEnvelop;
use crate::spec::TinyEncryptEnvelopType;
use rust_util::{debugging, iff, opt_result, opt_value_result, simple_error, XResult};
const TINY_ENCRYPT_KEY_PREFIX: &str = "tiny-encrypt-key:";
pub fn serialize_config_envelop(config_envelop: &TinyEncryptConfigEnvelop) -> String {
let mut s = String::new();
s.push_str(TINY_ENCRYPT_KEY_PREFIX);
s.push_str(config_envelop.r#type.get_name());
s.push(':');
s.push_str(&encode(config_envelop.sid.as_deref().unwrap_or("")));
s.push(':');
s.push_str(&encode(&config_envelop.kid));
s.push(':');
s.push_str(&encode(&config_envelop.public_part));
s
}
pub fn parse_temporary_keys(temporary_keys: &Option<Vec<String>>) -> XResult<Vec<TinyEncryptConfigEnvelop>> {
let mut temporary_envelops = vec![];
if let Some(temporary_key) = temporary_keys {
for t_key in temporary_key {
let envelop = opt_result!(deserialize_config_envelop(t_key), "Parse temporary key: {} failed: {}", t_key);
temporary_envelops.push(envelop);
}
debugging!("Temporary envelops: {:?}", temporary_envelops);
}
Ok(temporary_envelops)
}
pub fn deserialize_config_envelop(k: &str) -> XResult<TinyEncryptConfigEnvelop> {
if !k.starts_with(TINY_ENCRYPT_KEY_PREFIX) {
return simple_error!("invalid temporary key");
}
let k_parts = k.split(":").collect::<Vec<_>>();
if k_parts.len() != 5 {
return simple_error!("invalid temporary key (parts)");
}
let envelop_type = opt_value_result!(
TinyEncryptEnvelopType::from_name(k_parts[1]), "Unknown envelop type: {}", k_parts[1]);
Ok(TinyEncryptConfigEnvelop {
r#type: envelop_type,
sid: iff!(k_parts[2].is_empty(), None, Some(decode(k_parts[2])?)),
kid: decode(k_parts[3])?,
desc: None,
args: None,
public_part: decode(k_parts[4])?,
profiles: None,
})
}
fn encode(s: &str) -> String {
percent_encoding::utf8_percent_encode(s, percent_encoding::NON_ALPHANUMERIC).to_string()
}
fn decode(s: &str) -> XResult<String> {
Ok(opt_result!(
percent_encoding::percent_decode_str(s).decode_utf8(),
"decode: {} failed: {}",
s
)
.to_string())
}

View File

@@ -1,21 +1,129 @@
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::{fs, io}; use std::{fs, io};
use std::io::Write;
use std::path::Path;
use base64::Engine;
use base64::engine::general_purpose; use base64::engine::general_purpose;
use base64::Engine;
use dialoguer::console::Term;
use dialoguer::theme::ColorfulTheme;
use dialoguer::Confirm;
use pinentry::PassphraseInput;
use rand::random; use rand::random;
use rust_util::{simple_error, warning, XResult}; use rust_util::{information, opt_result, print_ex, simple_error, util_term, warning, XResult};
use secrecy::ExposeSecret;
use zeroize::Zeroize; use zeroize::Zeroize;
pub const ENC_AES256_GCM_P256: &str = "aes256-gcm-p256"; use crate::consts::{TINY_ENC_FILE_EXT, TINY_ENC_PEM_FILE_EXT};
pub const ENC_AES256_GCM_X25519: &str = "aes256-gcm-x25519"; use crate::util_digest::DigestWrite;
pub const TINY_ENC_FILE_EXT: &str = ".tinyenc"; use crate::util_env;
pub const TINY_ENC_CONFIG_FILE: &str = "~/.tinyencrypt/config-rs.json";
pub const TINY_ENC_AES_GCM: &str = "AES/GCM"; pub struct SecVec(pub Vec<u8>);
pub const TINY_ENC_MAGIC_TAG: u16 = 0x01; impl Drop for SecVec {
fn drop(&mut self) {
self.0.zeroize()
}
}
impl AsRef<[u8]> for SecVec {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
pub fn read_stdin() -> XResult<Vec<u8>> {
let mut buffer = vec![];
let mut stdin = io::stdin();
opt_result!(stdin.read_to_end(&mut buffer), "Read stdin failed: {}");
Ok(buffer)
}
pub fn read_pin(pin: &Option<String>) -> XResult<String> {
let mut ask_use_default_pin = true;
if let Some(pin) = pin {
if pin == "#INPUT#" {
ask_use_default_pin = false;
} else {
return Ok(pin.to_string());
}
}
if ask_use_default_pin && is_use_default_pin() {
return Ok("123456".into());
}
let rpin = {
let pin_entry = util_env::get_default_pin_entry().unwrap_or_else(|| "pinentry".to_string());
if let Some(mut input) = PassphraseInput::with_binary(pin_entry) {
let secret = input
.with_description("Please input your PIN.")
.with_prompt("PIN:")
.interact();
opt_result!(secret, "Read PIN from pinentry failed: {}")
.expose_secret()
.to_string()
} else {
opt_result!(rpassword::prompt_password("Please input PIN: "), "Read PIN failed: {}")
}
};
Ok(rpin)
}
pub fn read_password(password: &Option<String>) -> XResult<String> {
let rpassword = match password {
Some(pin) => pin.to_string(),
None => {
let pin_entry = util_env::get_default_pin_entry().unwrap_or_else(|| "pinentry".to_string());
if let Some(mut input) = PassphraseInput::with_binary(pin_entry) {
let secret = input
.with_description("Please input your password.")
.with_prompt("Password:")
.interact();
opt_result!(secret, "Read password from pinentry failed: {}")
.expose_secret()
.to_string()
} else {
opt_result!(rpassword::prompt_password("Please input password: "), "Read password failed: {}")
}
}
};
Ok(rpassword)
}
pub fn is_use_default_pin() -> bool {
if util_env::get_no_default_pin_hint() {
return false;
}
let use_dialoguer = util_env::get_use_dialoguer();
if use_dialoguer {
register_ctrlc();
let confirm_result = Confirm::with_theme(&ColorfulTheme::default())
.with_prompt("Use default PIN?")
.interact();
if confirm_result.is_err() {
let _ = Term::stderr().show_cursor();
}
confirm_result.unwrap_or(false)
} else {
util_term::read_yes_no("Use default PIN 123456, please confirm")
}
}
pub fn remove_file_with_msg(path: &PathBuf) {
match fs::remove_file(path) {
Err(e) => warning!("Remove file: {} failed: {}", path.display(), e),
Ok(_) => information!("Remove file: {} succeed", path.display()),
}
}
pub fn get_file_name(path: &Path) -> String {
let path_display = format!("{}", path.display());
if path_display.contains('/') {
if let Some(p) = path_display.split('/').last() {
return p.to_string();
}
}
path_display
}
pub fn require_tiny_enc_file_and_exists(path: impl AsRef<Path>) -> XResult<()> { pub fn require_tiny_enc_file_and_exists(path: impl AsRef<Path>) -> XResult<()> {
let path = path.as_ref(); let path = path.as_ref();
@@ -47,28 +155,64 @@ pub fn require_file_not_exists(path: impl AsRef<Path>) -> XResult<()> {
} }
} }
pub fn make_key256_and_nonce() -> (Vec<u8>, Vec<u8>) { pub fn make_nonce() -> SecVec {
let (_, nonce) = make_key256_and_nonce();
nonce
}
pub fn make_key256_and_nonce() -> (SecVec, SecVec) {
let key: [u8; 32] = random(); let key: [u8; 32] = random();
let nonce: [u8; 12] = random(); let nonce: [u8; 12] = random();
let result = (key.into(), nonce.into()); let key_vec: Vec<u8> = key.into();
let nonce_vec: Vec<u8> = nonce.into();
let (mut key, mut nonce) = (key, nonce); let (mut key, mut nonce) = (key, nonce);
key.zeroize(); key.zeroize();
nonce.zeroize(); nonce.zeroize();
result (SecVec(key_vec), SecVec(nonce_vec))
} }
pub fn simple_kdf(input: &[u8]) -> Vec<u8> { pub fn simple_kdf(input: &[u8]) -> Vec<u8> {
let input = hex::decode(sha256::digest(input)).unwrap(); let mut input = input.to_vec();
let input = hex::decode(sha256::digest(input)).unwrap(); for _ in 0..8 {
let input = hex::decode(sha256::digest(input)).unwrap(); let mut sha256 = DigestWrite::sha256();
let input = hex::decode(sha256::digest(input)).unwrap(); sha256.write_all(&input).expect("SHOULD NOT HAPPEN");
let input = hex::decode(sha256::digest(input)).unwrap(); input = sha256.digest();
let input = hex::decode(sha256::digest(input)).unwrap(); }
let input = hex::decode(sha256::digest(input)).unwrap();
let input = hex::decode(sha256::digest(input)).unwrap();
input input
} }
pub fn parse_pem(pem: &str) -> XResult<Vec<u8>> {
let mut pem = pem.trim().to_owned();
if pem.starts_with("-----BEGIN") {
let mut filter_lines = vec![];
let lines = pem.lines().skip(1);
for ln in lines {
if ln.starts_with("-----END") {
break;
} else {
filter_lines.push(ln.to_string());
}
}
pem = filter_lines.join("");
}
pem = pem.chars().filter(|c| *c != '\n' && *c != '\r').clone().collect::<String>();
Ok(opt_result!(decode_base64(&pem), "Decode PEM failed: {}"))
}
pub fn to_pem(bs: &[u8], name: &str) -> String {
let bs_base64 = encode_base64(bs);
let mut pem = String::with_capacity(bs.len() + 64);
pem.push_str(&format!("-----BEGIN {}-----", name));
for (i, c) in bs_base64.chars().enumerate() {
if i % 64 == 0 { pem.push('\n'); }
pem.push(c);
}
if !pem.ends_with('\n') { pem.push('\n'); }
pem.push_str(&format!("-----END {}-----", name));
pem
}
pub fn decode_base64(input: &str) -> XResult<Vec<u8>> { pub fn decode_base64(input: &str) -> XResult<Vec<u8>> {
Ok(general_purpose::STANDARD.decode(input)?) Ok(general_purpose::STANDARD.decode(input)?)
} }
@@ -87,7 +231,7 @@ pub fn decode_base64_url_no_pad(input: &str) -> XResult<Vec<u8>> {
pub fn read_number(hint: &str, from: usize, to: usize) -> usize { pub fn read_number(hint: &str, from: usize, to: usize) -> usize {
loop { loop {
print!("{} ({}-{}): ", hint, from, to); print_ex!("{} ({}-{}): ", hint, from, to);
io::stdout().flush().ok(); io::stdout().flush().ok();
let mut buff = String::new(); let mut buff = String::new();
let _ = io::stdin().read_line(&mut buff).expect("Read line from stdin"); let _ = io::stdin().read_line(&mut buff).expect("Read line from stdin");
@@ -103,33 +247,104 @@ pub fn read_number(hint: &str, from: usize, to: usize) -> usize {
} }
} }
pub fn is_tiny_enc_file(filename: &str) -> bool {
filename.ends_with(TINY_ENC_FILE_EXT) || filename.ends_with(TINY_ENC_PEM_FILE_EXT)
}
pub fn get_user_agent() -> String { pub fn get_user_agent() -> String {
format!("TinyEncrypt-rs v{}@{}", env!("CARGO_PKG_VERSION"), format!("TinyEncrypt-rs v{}@{}-{}", env!("CARGO_PKG_VERSION"), get_os(), get_arch())
if cfg!(target_os = "macos") { }
"MacOS"
} else if cfg!(target_os = "ios") { pub fn get_os() -> String {
"iOS" if cfg!(target_os = "macos") {
} else if cfg!(target_os = "android") { "macOS"
"Android" } else if cfg!(target_os = "ios") {
} else if cfg!(target_os = "windows") { "iOS"
"Windows" } else if cfg!(target_os = "android") {
} else if cfg!(target_os = "linux") { "Android"
"Linux" } else if cfg!(target_os = "windows") {
} else if cfg!(target_os = "freebsd") { "Windows"
"FreeBSD" } else if cfg!(target_os = "linux") {
} else if cfg!(target_os = "dragonfly") { "Linux"
"Dragonfly" } else if cfg!(target_os = "freebsd") {
} else if cfg!(target_os = "openbsd") { "FreeBSD"
"OpenBSD" } else if cfg!(target_os = "dragonfly") {
} else if cfg!(target_os = "netbsd") { "Dragonfly"
"NetBSD" } else if cfg!(target_os = "openbsd") {
} else { "OpenBSD"
panic!("Unsupported OS!"); } else if cfg!(target_os = "netbsd") {
} "NetBSD"
) } else {
"UnknownOS"
}.to_string()
}
pub fn get_arch() -> String {
if cfg!(target_arch = "x86_64") {
"x86-64"
} else if cfg!(target_arch = "x86") {
"x86"
} else if cfg!(target_arch = "aarch64") {
"aarch64"
} else if cfg!(target_arch = "arm") {
"arm"
} else if cfg!(target_arch = "riscv64") {
"riscv64"
} else if cfg!(target_arch = "riscv32") {
"riscv32"
} else if cfg!(target_arch = "mips64") {
"mips64"
} else if cfg!(target_arch = "mips") {
"mips"
} else if cfg!(target_arch = "powerpc64") {
"powerpc64"
} else if cfg!(target_arch = "powerpc") {
"powerpc"
} else {
"unknown"
}.to_string()
} }
pub fn zeroize(object: impl Zeroize) { pub fn zeroize(object: impl Zeroize) {
let mut object = object; let mut object = object;
object.zeroize(); object.zeroize();
} }
pub fn read_line(ln: &str) {
print_ex!("{}", ln);
io::stdout().flush().ok();
let mut buff = String::new();
let _ = io::stdin().read_line(&mut buff).expect("Read line from stdin");
}
pub fn ratio(numerator: u64, denominator: u64) -> String {
if denominator == 0 {
return "".to_string();
}
let r = (numerator * 10000) / denominator;
format!("{:.2}", r as f64 / 100f64)
}
#[allow(clippy::declare_interior_mutable_const)]
const CTRL_C_SET: AtomicBool = AtomicBool::new(false);
pub fn register_ctrlc() {
#[allow(clippy::borrow_interior_mutable_const)]
if !CTRL_C_SET.load(Ordering::SeqCst) {
CTRL_C_SET.store(true, Ordering::SeqCst);
let _ = ctrlc::set_handler(move || {
// DO NOTHING
});
}
}
#[test]
fn test_simple_kdf() {
assert_eq!("30edbc354e8cf656adcddbeefbf3f5073372cdc42e4eca2e797bda8abebb6a05",
hex::encode(simple_kdf(b"")));
assert_eq!("0624d2a57bcb50f70aa19bab9fa75af1ca66cc701c341df865d430e2e6d9d936",
hex::encode(simple_kdf(b"hello")));
assert_eq!("43367d255eddedc3c84b692b68de6d3d21da28caad6abd20ed85a4f2c89706ad",
hex::encode(simple_kdf(b"hello world")));
}

83
src/util_digest.rs Normal file
View File

@@ -0,0 +1,83 @@
use std::io::Write;
use std::iter::repeat;
use crypto::digest::Digest;
use crypto::md5::Md5;
use crypto::ripemd160::Ripemd160;
use crypto::sha1::Sha1;
use crypto::sha2::{Sha224, Sha256, Sha384, Sha512, Sha512Trunc224, Sha512Trunc256};
use crypto::sha3::Sha3;
use crypto::whirlpool::Whirlpool;
use rust_util::{simple_error, XResult};
pub struct DigestWrite {
digest: Box<dyn Digest>,
}
impl Write for DigestWrite {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.digest.input(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
impl DigestWrite {
pub fn from_algo(algo: &str) -> XResult<Self> {
match get_digest_by_algorithm(algo) {
None => simple_error!("Unsupported algo: {}", algo),
Some(digest) => Ok(Self { digest })
}
}
pub fn sha256() -> Self {
Self { digest: Box::new(Sha256::new()) }
}
pub fn digest(self) -> Vec<u8> {
let mut digest = self.digest;
let mut buf: Vec<u8> = repeat(0).take((digest.output_bits() + 7) / 8).collect();
digest.result(&mut buf);
buf
}
}
pub fn sha256_digest(input: &[u8]) -> Vec<u8> {
let mut digest = Sha256::new();
digest.input(input);
let mut buf: Vec<u8> = repeat(0).take((digest.output_bits() + 7) / 8).collect();
digest.result(&mut buf);
buf
}
fn get_digest_by_algorithm(algo: &str) -> Option<Box<dyn Digest>> {
let algo = algo.to_uppercase();
match algo.as_str() {
"RIPEMD160" => Some(Box::new(Ripemd160::new())),
"WHIRLPOOL" => Some(Box::new(Whirlpool::new())),
// "BLAKE2S" => Some(Box::new(Blake2s::new(iff!(options.blake_len == 0_usize, 32, options.blake_len)))),
// "BLAKE2B" => Some(Box::new(Blake2b::new(iff!(options.blake_len == 0_usize, 64, options.blake_len)))),
"MD5" => Some(Box::new(Md5::new())),
"SHA1" | "SHA-1" => Some(Box::new(Sha1::new())),
"SHA224" | "SHA-224" => Some(Box::new(Sha224::new())),
"SHA256" | "SHA-256" => Some(Box::new(Sha256::new())),
"SHA384" | "SHA-384" => Some(Box::new(Sha384::new())),
"SHA512" | "SHA-512" => Some(Box::new(Sha512::new())),
"SHA512-224" => Some(Box::new(Sha512Trunc224::new())),
"SHA512-256" => Some(Box::new(Sha512Trunc256::new())),
"SHA3-224" => Some(Box::new(Sha3::sha3_224())),
"SHA3-256" => Some(Box::new(Sha3::sha3_256())),
"SHA3-384" => Some(Box::new(Sha3::sha3_384())),
"SHA3-512" => Some(Box::new(Sha3::sha3_512())),
"SHAKE-128" => Some(Box::new(Sha3::shake_128())),
"SHAKE-256" => Some(Box::new(Sha3::shake_256())),
"KECCAK-224" => Some(Box::new(Sha3::keccak224())),
"KECCAK-256" => Some(Box::new(Sha3::keccak256())),
"KECCAK-384" => Some(Box::new(Sha3::keccak384())),
"KECCAK-512" => Some(Box::new(Sha3::keccak512())),
_ => None,
}
}

View File

@@ -1,35 +1,80 @@
use p256::ecdh::EphemeralSecret; pub mod ecdh_p256 {
use rand::rngs::OsRng; use std::ops::Deref;
use rust_util::{opt_result, XResult}; use p256::{EncodedPoint, PublicKey};
use p256::ecdh::EphemeralSecret;
use p256::elliptic_curve::sec1::FromEncodedPoint;
use p256::pkcs8::EncodePublicKey;
use rand::rngs::OsRng;
use rust_util::{opt_result, XResult};
use p256::pkcs8::EncodePublicKey; pub fn compute_p256_shared_secret(public_key_point_hex: &str) -> XResult<(Vec<u8>, Vec<u8>)> {
use p256::{EncodedPoint, PublicKey}; let public_key_point_bytes = opt_result!(hex::decode(public_key_point_hex), "Parse public key point hex failed: {}");
use p256::elliptic_curve::sec1::FromEncodedPoint; let encoded_point = opt_result!(EncodedPoint::from_bytes(public_key_point_bytes), "Parse public key point failed: {}");
// use p256::elliptic_curve::sec1::{FromEncodedPoint, ToEncodedPoint}; let public_key = PublicKey::from_encoded_point(&encoded_point).unwrap();
// #[derive(Debug)] let esk = EphemeralSecret::random(&mut OsRng);
// pub struct EphemeralKeyBytes(EncodedPoint); let epk = esk.public_key();
// let shared_secret = esk.diffie_hellman(&public_key);
// impl EphemeralKeyBytes { let epk_public_key_der = opt_result!(epk.to_public_key_der(), "Convert epk to SPKI failed: {}");
// pub fn from_public_key(epk: &PublicKey) -> Self { Ok((shared_secret.raw_secret_bytes().deref().to_vec(), epk_public_key_der.to_vec()))
// EphemeralKeyBytes(epk.to_encoded_point(true)) }
// } }
//
// pub fn decompress(&self) -> EncodedPoint {
// // EphemeralKeyBytes is a valid compressed encoding by construction.
// let p = PublicKey::from_encoded_point(&self.0).unwrap();
// p.to_encoded_point(false)
// }
// }
pub fn compute_shared_secret(public_key_point_hex: &str) -> XResult<(Vec<u8>, Vec<u8>)> { pub mod ecdh_p384 {
let public_key_point_bytes = opt_result!(hex::decode(public_key_point_hex), "Parse public key point hex failed: {}"); use std::ops::Deref;
let encoded_point = opt_result!(EncodedPoint::from_bytes(&public_key_point_bytes), "Parse public key point failed: {}"); use p384::{EncodedPoint, PublicKey};
let public_key = PublicKey::from_encoded_point(&encoded_point).unwrap(); use p384::ecdh::EphemeralSecret;
use p384::elliptic_curve::sec1::FromEncodedPoint;
use p384::pkcs8::EncodePublicKey;
use rand::rngs::OsRng;
use rust_util::{opt_result, XResult};
let esk = EphemeralSecret::random(&mut OsRng); pub fn compute_p384_shared_secret(public_key_point_hex: &str) -> XResult<(Vec<u8>, Vec<u8>)> {
let epk = esk.public_key(); let public_key_point_bytes = opt_result!(hex::decode(public_key_point_hex), "Parse public key point hex failed: {}");
let shared_secret = esk.diffie_hellman(&public_key); let encoded_point = opt_result!(EncodedPoint::from_bytes(public_key_point_bytes), "Parse public key point failed: {}");
let epk_public_key_der = opt_result!(epk.to_public_key_der(), "Convert epk to SPKI failed: {}"); let public_key = PublicKey::from_encoded_point(&encoded_point).unwrap();
Ok((shared_secret.raw_secret_bytes().as_slice().to_vec(), epk_public_key_der.to_vec()))
} let esk = EphemeralSecret::random(&mut OsRng);
let epk = esk.public_key();
let shared_secret = esk.diffie_hellman(&public_key);
let epk_public_key_der = opt_result!(epk.to_public_key_der(), "Convert epk to SPKI failed: {}");
Ok((shared_secret.raw_secret_bytes().deref().to_vec(), epk_public_key_der.to_vec()))
}
}
pub mod ecdh_x25519 {
use rand::rngs::OsRng;
use rust_util::{opt_result, simple_error, XResult};
use x25519_dalek::{EphemeralSecret, PublicKey};
pub fn compute_x25519_shared_secret(public_key_point_hex: &str) -> XResult<(Vec<u8>, Vec<u8>)> {
let public_key_bytes = opt_result!(hex::decode(public_key_point_hex), "Parse X25519 public key hex failed: {}");
if public_key_bytes.len() != 32 {
return simple_error!("Parse X25519 key failed: not 32 bytes");
}
let public_key_bytes: [u8; 32] = public_key_bytes.try_into().unwrap();
let public_key_card = PublicKey::from(public_key_bytes);
let ephemeral_secret = EphemeralSecret::random_from_rng(OsRng);
let ephemeral_public = PublicKey::from(&ephemeral_secret);
let shared_secret = ephemeral_secret.diffie_hellman(&public_key_card);
Ok((shared_secret.as_bytes().to_vec(), ephemeral_public.as_bytes().to_vec()))
}
}
pub mod ecdh_kyber1024 {
use pqcrypto_kyber::kyber1024;
use pqcrypto_kyber::kyber1024::PublicKey as Kyber1024PublicKey;
use pqcrypto_traits::kem::{Ciphertext, PublicKey, SharedSecret};
use rust_util::{opt_result, XResult};
pub fn compute_kyber1024_shared_secret(public_key_point_hex: &str) -> XResult<(Vec<u8>, Vec<u8>)> {
let public_key_bytes = opt_result!(hex::decode(public_key_point_hex), "Parse Kyber1024 public key hex failed: {}");
let public_key = opt_result!(Kyber1024PublicKey::from_bytes(&public_key_bytes), "Parse Kyber1024 public key failed: {}");
let (shared_secret, ciphertext) = kyber1024::encapsulate(&public_key);
Ok((shared_secret.as_bytes().to_vec(), ciphertext.as_bytes().to_vec()))
}
}

61
src/util_enc_file.rs Normal file
View File

@@ -0,0 +1,61 @@
use std::io::{Read, Write};
use rust_util::{debugging, iff, opt_result, simple_error, XResult};
use crate::compress;
use crate::consts::{TINY_ENC_COMPRESSED_MAGIC_TAG, TINY_ENC_MAGIC_TAG};
use crate::spec::TinyEncryptMeta;
pub fn write_tiny_encrypt_meta(write: &mut impl Write, meta: &TinyEncryptMeta, compress_meta: bool) -> XResult<usize> {
let meta_tag = iff!(compress_meta, TINY_ENC_COMPRESSED_MAGIC_TAG, TINY_ENC_MAGIC_TAG);
opt_result!(write.write_all(&meta_tag.to_be_bytes()), "Write tag failed: {}");
let mut encrypted_meta_bytes = opt_result!(serde_json::to_vec(&meta), "Generate meta json bytes failed: {}");
if compress_meta {
encrypted_meta_bytes = opt_result!(
compress::compress_default(&encrypted_meta_bytes), "Compress encrypted meta failed: {}");
}
let encrypted_meta_bytes_len = encrypted_meta_bytes.len() as u32;
debugging!("Encrypted meta len: {}", encrypted_meta_bytes_len);
opt_result!(write.write_all(&encrypted_meta_bytes_len.to_be_bytes()), "Write meta len failed: {}");
opt_result!(write.write_all(&encrypted_meta_bytes), "Write meta failed: {}");
Ok(encrypted_meta_bytes.len() + 2 + 4)
}
pub fn read_tiny_encrypt_meta_and_normalize(r: &mut impl Read) -> XResult<(u32, bool, TinyEncryptMeta)> {
let mut meta_len_and_meta = read_tiny_encrypt_meta(r);
let _ = meta_len_and_meta.as_mut().map(|ml| ml.2.normalize_envelops());
meta_len_and_meta
}
pub fn read_tiny_encrypt_meta(r: &mut impl Read) -> XResult<(u32, bool, TinyEncryptMeta)> {
let mut meta_tag_buff = [0_u8; 2];
opt_result!(r.read_exact(&mut meta_tag_buff), "Read tag failed: {}");
let meta_tag = u16::from_be_bytes(meta_tag_buff);
debugging!("Found tag: {}", meta_tag);
let is_meta_normal = meta_tag == TINY_ENC_MAGIC_TAG;
let is_meta_compressed = meta_tag == TINY_ENC_COMPRESSED_MAGIC_TAG;
if !is_meta_normal && !is_meta_compressed {
return simple_error!("Tag is not 0x01 or 0x02, but is: 0x{:x}", meta_tag);
}
let mut meta_length_buff = [0_u8; 4];
opt_result!(r.read_exact(&mut meta_length_buff), "Read length failed: {}");
let meta_length = u32::from_be_bytes(meta_length_buff);
if meta_length > 10 * 1024 * 1024 {
return simple_error!("Meta too large: {}", meta_length);
}
debugging!("Encrypted meta length: {}", meta_length);
let mut meta_buff = vec![0; meta_length as usize];
opt_result!(r.read_exact(meta_buff.as_mut_slice()), "Read meta failed: {}");
debugging!("Tiny enc meta compressed: {}", is_meta_compressed);
if is_meta_compressed {
meta_buff = opt_result!(compress::decompress(&meta_buff), "Decompress meta failed: {}");
debugging!("Encrypted meta decompressed: {} byte(s) -> {} byte(s)", meta_length, meta_buff.len());
}
debugging!("Encrypted meta: {}", String::from_utf8_lossy(&meta_buff));
Ok((meta_length, is_meta_compressed, opt_result!(serde_json::from_slice(&meta_buff), "Parse meta failed: {}")))
}

91
src/util_env.rs Normal file
View File

@@ -0,0 +1,91 @@
use std::{env, fs};
use rust_util::util_env as rust_util_env;
use rust_util::{debugging, util_env, warning};
use crate::consts;
pub const TINY_ENCRYPT_ENV_DEFAULT_ALGORITHM: &str = "TINY_ENCRYPT_DEFAULT_ALGORITHM";
pub const TINY_ENCRYPT_ENV_DEFAULT_COMPRESS: &str = "TINY_ENCRYPT_DEFAULT_COMPRESS";
pub const TINY_ENCRYPT_ENV_NO_PROGRESS: &str = "TINY_ENCRYPT_NO_PROGRESS";
pub const TINY_ENCRYPT_ENV_USE_DIALOGUER: &str = "TINY_ENCRYPT_USE_DIALOGUER";
pub const TINY_ENCRYPT_ENV_PIN: &str = "TINY_ENCRYPT_PIN";
pub const TINY_ENCRYPT_ENV_KEY_ID: &str = "TINY_ENCRYPT_KEY_ID";
pub const TINY_ENCRYPT_ENV_AUTO_SELECT_KEY_IDS: &str = "TINY_ENCRYPT_AUTO_SELECT_KEY_IDS";
pub const TINY_ENCRYPT_EVN_AUTO_COMPRESS_EXTS: &str = "TINY_ENCRYPT_AUTO_COMPRESS_EXTS";
pub const TINY_ENCRYPT_ENV_GPG_COMMAND: &str = "TINY_ENCRYPT_GPG_COMMAND";
pub const TINY_ENCRYPT_ENV_NO_DEFAULT_PIN_HINT: &str = "TINY_ENCRYPT_NO_DEFAULT_PIN_HINT";
pub const TINY_ENCRYPT_ENV_PIN_ENTRY: &str = "TINY_ENCRYPT_PIN_ENTRY";
pub const TINY_ENCRYPT_ENV_EXTERNAL_COMMAND: &str = "TINY_ENCRYPT_EXTERNAL_COMMAND";
pub fn get_default_encryption_algorithm() -> Option<&'static str> {
let env_default_algorithm = rust_util_env::env_var(TINY_ENCRYPT_ENV_DEFAULT_ALGORITHM);
debugging!("Environment variable {} = {:?}", TINY_ENCRYPT_ENV_DEFAULT_ALGORITHM, env_default_algorithm);
if let Some(env_algorithm) = env_default_algorithm {
let lower_default_algorithm = env_algorithm.to_lowercase();
match lower_default_algorithm.as_str() {
"aes" | "aes/gcm" => return Some(consts::TINY_ENC_AES_GCM),
"chacha20" | "chacha20/poly1305" => return Some(consts::TINY_ENC_CHACHA20_POLY1305),
_ => { warning!("Not matched any algorithm: {}", env_algorithm); }
}
}
None
}
pub fn get_pin() -> Option<String> {
env::var(TINY_ENCRYPT_ENV_PIN).ok()
}
pub fn get_key_id() -> Option<String> {
rust_util_env::env_var(TINY_ENCRYPT_ENV_KEY_ID)
}
pub fn get_gpg_cmd() -> Option<String> {
rust_util_env::env_var(TINY_ENCRYPT_ENV_GPG_COMMAND)
}
pub fn get_default_pin_entry() -> Option<String> {
let default_pin_entry = "/usr/local/MacGPG2/libexec/pinentry-mac.app/Contents/MacOS/pinentry-mac";
if let Ok(meta) = fs::metadata(default_pin_entry) {
if meta.is_file() {
return Some(default_pin_entry.to_string());
}
}
get_pin_entry()
}
pub fn get_pin_entry() -> Option<String> {
rust_util_env::env_var(TINY_ENCRYPT_ENV_PIN_ENTRY)
}
pub fn get_auto_select_key_ids() -> Option<Vec<String>> {
get_env_with_split(TINY_ENCRYPT_ENV_AUTO_SELECT_KEY_IDS)
}
pub fn get_auto_compress_file_exts() -> Option<Vec<String>> {
get_env_with_split(TINY_ENCRYPT_EVN_AUTO_COMPRESS_EXTS)
}
pub fn get_default_compress() -> Option<bool> {
rust_util_env::env_var(TINY_ENCRYPT_ENV_DEFAULT_COMPRESS).map(|val| util_env::is_on(&val))
}
pub fn get_no_progress() -> bool {
rust_util_env::is_env_on(TINY_ENCRYPT_ENV_NO_PROGRESS)
}
pub fn get_no_default_pin_hint() -> bool {
rust_util_env::is_env_on(TINY_ENCRYPT_ENV_NO_DEFAULT_PIN_HINT)
}
pub fn get_use_dialoguer() -> bool {
rust_util_env::is_env_on(TINY_ENCRYPT_ENV_USE_DIALOGUER)
}
fn get_env_with_split(env_name: &str) -> Option<Vec<String>> {
let val = rust_util_env::env_var(env_name);
debugging!("Environment variable {} = {:?}", env_name, &val);
val.map(|env_values| {
env_values.split(',').map(ToString::to_string).collect::<Vec<_>>()
})
}

34
src/util_envelop.rs Normal file
View File

@@ -0,0 +1,34 @@
use rust_util::iff;
use crate::config::{TinyEncryptConfig, TinyEncryptConfigEnvelop};
use crate::spec::TinyEncryptEnvelop;
pub fn format_envelop(envelop: &TinyEncryptEnvelop, config: &Option<TinyEncryptConfig>) -> String {
let config_envelop = config.as_ref().and_then(|c| c.find_by_kid(&envelop.kid));
let envelop_kid = config_envelop.and_then(|e| e.sid.as_ref())
.map(|sid| format!(", Sid: {}", sid))
.unwrap_or_else(|| format!(", Kid: {}", envelop.kid));
let envelop_desc = get_envelop_desc(envelop, &config_envelop);
let desc = envelop_desc.as_ref()
.map(|desc| format!(", Desc: {}", desc))
.unwrap_or_default();
format!("{}{}{}", with_width_type(&envelop.r#type.get_upper_name()), envelop_kid, desc)
}
fn get_envelop_desc(envelop: &TinyEncryptEnvelop, config_envelop: &Option<&TinyEncryptConfigEnvelop>) -> Option<String> {
if let Some(desc) = &envelop.desc {
return Some(desc.to_string());
}
if let Some(config_envelop) = config_envelop {
return config_envelop.desc.clone();
}
None
}
pub fn with_width_type(s: &str) -> String {
with_width(s, 13)
}
pub fn with_width(s: &str, width: usize) -> String {
iff!(s.len() < width, format!("{}{}", s, " ".repeat(width - s.len())), s.to_string())
}

25
src/util_file.rs Normal file
View File

@@ -0,0 +1,25 @@
use std::time::SystemTime;
use fs_set_times::SystemTimeSpec;
use rust_util::{information, warning};
use rust_util::util_time::UnixEpochTime;
use crate::spec::EncEncryptedMeta;
pub fn update_file_time(enc_meta: Option<EncEncryptedMeta>, path: &str) {
if let Some(enc_meta) = &enc_meta {
let create_time = enc_meta.c_time.map(SystemTime::from_millis);
let modify_time = enc_meta.m_time.map(SystemTime::from_millis);
if create_time.is_some() || modify_time.is_some() {
let set_times_result = fs_set_times::set_times(
path,
create_time.map(SystemTimeSpec::Absolute),
modify_time.map(SystemTimeSpec::Absolute),
);
match set_times_result {
Ok(_) => information!("Set file time succeed."),
Err(e) => warning!("Set file time failed: {}", e),
}
}
}
}

107
src/util_gpg.rs Normal file
View File

@@ -0,0 +1,107 @@
// Command encrypt use GnuPG(GPG)
/*
echo message | gpg -r KEY_ID -e -a --no-comment --comment "tiny-encrypt-1.6.0 - KEY_ID"
Success message:
-----BEGIN PGP MESSAGE-----
Comment: tiny-encrypt-1.6.0 - C0FAD5E563B80E819603B0D9FFC2A910806894FD
hF4DcCBclRkzzAMSAQdA6nLd40IPxZF62Q54t2bpwvFXsG0Wy6SYxGEp1/K6rWgw
jgSx2ZiCntadkFrH35MJAYOx/DVW6ngxIic8hO+liBZfqI1lv7vlvVfs4sAe1bqK
0kcBDqp5SGNx2ENiDA4IbqDAp7JppQpEZrWJd2FGdbKviRyprVYgGILGJcMZQVNJ
agJfGGj7HPf5IbffrWWWyfE7oNCkSDZ2bw==
=tANz
-----END PGP MESSAGE-----
Failed message:
gpg: C0FAD5E563B80E819603B0D9FFC2A910806894FF: skipped: No public key
gpg: [stdin]: encryption failed: No public key
*/
// Command decrypt use GnuPG(GPG)
/*
echo '-----BEGIN PGP MESSAGE-----
Comment: tiny-encrypt-1.6.0
hF4DcCBclRkzzAMSAQdAESdgetyKsgdAR6kps5ThpP2TcZB0hyGrmDqGj/1+lXIw
c9cam+BxFkDT7mZafuls0tV4MwHwKi2z1gQFNgTWuC45rpXyK7BFg74Rua+qLzvJ
0kcBpKSZIvQ/lX8JQ4hM41k6ymeYBQMC2nzmhwl/g9NBFyn5+dlEzDiZvL8YyQFT
IDGbLcEBW7a0B02ZKZ4ELyIDp94hdcbhrg==
=QEBj
-----END PGP MESSAGE-----' | gpg -d
Failed message:
gpg: encrypted with 256-bit ECDH key, ID 70205C951933CC03, created 2023-10-05
"Hatter Jiang (2023) <jht5945@gmail.com>"
gpg: public key decryption failed: Operation cancelled
gpg: decryption failed: No secret key
*/
use std::io::Write;
use std::process::{Command, Stdio};
use rust_util::{opt_result, opt_value_result, simple_error, XResult};
use crate::util_env;
pub fn gpg_encrypt(key_id: &str, message: &[u8]) -> XResult<String> {
let message_hex = hex::encode(message);
let mut cmd = Command::new(get_gpg_cmd());
let gpg_encrypt_result = cmd
.args([
"-e", "-a", "--no-comment",
"-r", key_id,
"--comment", &format!("tiny-encrypt-v{} - {}", env!("CARGO_PKG_VERSION"), key_id)
])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn();
let gpg_encrypt = opt_result!(gpg_encrypt_result, "Run GPG encrypt failed: {}");
let mut gpg_encrypt_stdin = opt_value_result!(gpg_encrypt.stdin.as_ref(), "Get GPG encrypt stdin failed.");
opt_result!(gpg_encrypt_stdin.write_all(message_hex.as_bytes()), "Write GPG encrypt stdin failed: {}");
let encrypt_result = gpg_encrypt.wait_with_output();
let encrypt_output = opt_result!(encrypt_result, "GPG encrypt failed: {}");
let stdout = String::from_utf8_lossy(&encrypt_output.stdout).to_string();
let stderr = String::from_utf8_lossy(&encrypt_output.stderr).to_string();
if !encrypt_output.status.success() {
return simple_error!(
"GPG encrypt failed:\n- exit code: [{:?}]\n- stdout: [{}]\n- stderr: [{}]",
encrypt_output.status.code(), stdout, stderr
);
}
Ok(stdout)
}
pub fn gpg_decrypt(message: &str) -> XResult<Vec<u8>> {
let mut cmd = Command::new(get_gpg_cmd());
let gpg_decrypt_result = cmd
.arg("-d")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn();
let gpg_encrypt = opt_result!(gpg_decrypt_result, "Run GPG decrypt failed: {}");
let mut gpg_encrypt_stdin = opt_value_result!(gpg_encrypt.stdin.as_ref(), "Get GPG decrypt stdin failed.");
opt_result!(gpg_encrypt_stdin.write_all(message.as_bytes()), "Write GPG decrypt stdin failed: {}");
let decrypt_result = gpg_encrypt.wait_with_output();
let decrypt_output = opt_result!(decrypt_result, "GPG decrypt failed: {}");
let stdout = String::from_utf8_lossy(&decrypt_output.stdout).to_string();
let stderr = String::from_utf8_lossy(&decrypt_output.stderr).to_string();
if !decrypt_output.status.success() {
return simple_error!(
"GPG decrypt failed:\n- exit code: [{:?}]\n- stdout: [{}]\n- stderr: [{}]",
decrypt_output.status.code(), stdout, stderr
);
}
let decrypted = opt_result!(hex::decode(stdout.trim()), "Decode decrypted message failed: {}");
Ok(decrypted)
}
fn get_gpg_cmd() -> String {
util_env::get_gpg_cmd().unwrap_or("gpg".to_string())
}

47
src/util_keychainkey.rs Normal file
View File

@@ -0,0 +1,47 @@
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use rust_util::{simple_error, XResult};
use swift_secure_enclave_tool_rs::{ControlFlag, KeyMlKem, KeyPurpose};
use crate::spec::TinyEncryptEnvelopType;
pub fn is_support_se() -> bool {
swift_secure_enclave_tool_rs::is_secure_enclave_supported().unwrap_or(false)
}
pub fn decrypt_data(
envelop_type: TinyEncryptEnvelopType,
private_key_base64: &str,
ephemeral_public_key_bytes: &[u8],
) -> XResult<Vec<u8>> {
let private_key_representation = STANDARD.decode(private_key_base64)?;
let shared_secret = match envelop_type {
TinyEncryptEnvelopType::KeyP256 => swift_secure_enclave_tool_rs::private_key_ecdh(
&private_key_representation,
ephemeral_public_key_bytes,
)?,
TinyEncryptEnvelopType::KeyMlKem768 => swift_secure_enclave_tool_rs::private_key_mlkem_ecdh(
KeyMlKem::MlKem768,
&private_key_representation,
ephemeral_public_key_bytes,
)?,
TinyEncryptEnvelopType::KeyMlKem1024 => swift_secure_enclave_tool_rs::private_key_mlkem_ecdh(
KeyMlKem::MlKem1024,
&private_key_representation,
ephemeral_public_key_bytes,
)?,
_ => return simple_error!("Invalid envelop type: {:?}", envelop_type),
};
Ok(shared_secret)
}
pub fn generate_se_p256_keypair(control_flag: ControlFlag) -> XResult<(String, String)> {
if !is_support_se() {
return simple_error!("Secure enclave is not supported.");
}
let key_material =
swift_secure_enclave_tool_rs::generate_keypair(KeyPurpose::KeyAgreement, control_flag)?;
Ok((
hex::encode(&key_material.public_key_point),
STANDARD.encode(&key_material.private_key_representation),
))
}

294
src/util_keychainstatic.rs Normal file
View File

@@ -0,0 +1,294 @@
use std::path::PathBuf;
use pqcrypto_kyber::kyber1024;
use pqcrypto_kyber::kyber1024::Ciphertext as Kyber1024Ciphertext;
use pqcrypto_kyber::kyber1024::PublicKey as Kyber1024PublicKey;
use pqcrypto_kyber::kyber1024::SecretKey as Kyber1024SecretKey;
use rust_util::{debugging, opt_result, opt_value_result, simple_error, util_file, XResult};
use security_framework::os::macos::keychain::{CreateOptions, SecKeychain};
use x25519_dalek::{PublicKey, StaticSecret};
use zeroize::Zeroize;
const X2559_PLAIN_PREFIX: &str = "x25519-plain:";
const KYBER1024_PLAIN_PREFIX: &str = "kyber1024-plain:";
const KEYCHAIN_KEY_PREFIX: &str = "keychain:";
pub struct KeychainKey {
pub keychain_name: String,
pub service_name: String,
pub key_name: String,
}
pub enum KeychainStaticSecretAlgorithm {
X25519,
Kyber1024,
}
impl KeychainStaticSecretAlgorithm {
pub fn prefix(&self) -> &'static str {
match self {
Self::X25519 => X2559_PLAIN_PREFIX,
Self::Kyber1024 => KYBER1024_PLAIN_PREFIX,
}
}
pub fn from_prefix(str: &str) -> Option<Self> {
if str.starts_with(X2559_PLAIN_PREFIX) {
Some(Self::X25519)
} else if str.starts_with(KYBER1024_PLAIN_PREFIX) {
Some(Self::Kyber1024)
} else {
None
}
}
}
pub struct KeychainStaticSecret {
pub algo: KeychainStaticSecretAlgorithm,
pub secret: Vec<u8>,
pub public: Option<Vec<u8>>,
}
impl Zeroize for KeychainStaticSecret {
fn zeroize(&mut self) {
self.secret.zeroize();
}
}
impl KeychainStaticSecret {
pub fn parse_bytes(bs: &[u8]) -> XResult<Self> {
let key_str = opt_result!(String::from_utf8(bs.to_vec()), "Parse static secret failed: {}");
Self::parse(&key_str)
}
pub fn parse(key: &str) -> XResult<Self> {
let algo = opt_value_result!(
KeychainStaticSecretAlgorithm::from_prefix(key), "Unknown static secret: {}", key);
let extract_key_hex = &key[algo.prefix().len()..];
let extract_key = opt_result!(hex::decode(extract_key_hex), "Decode static secret plain key failed: {}");
let (secret, public) = match algo {
KeychainStaticSecretAlgorithm::X25519 => {
(extract_key, None)
}
KeychainStaticSecretAlgorithm::Kyber1024 => {
// pub const PQCLEAN_KYBER1024_CLEAN_CRYPTO_SECRETKEYBYTES: usize = 3168;
// pub const PQCLEAN_KYBER1024_CLEAN_CRYPTO_PUBLICKEYBYTES: usize = 1568;
let secret_key_bytes_len = 3168;
let public_key_bytes_len = 1568;
if extract_key.len() != secret_key_bytes_len + public_key_bytes_len {
return simple_error!("Bad kyber1024 secret and public keys.");
}
(extract_key[0..secret_key_bytes_len].to_vec(), Some(extract_key[secret_key_bytes_len..].to_vec()))
}
};
Ok(Self {
algo,
secret,
public,
})
}
pub fn to_str(&self) -> String {
let mut v = String::new();
v.push_str(self.algo.prefix());
v.push_str(&hex::encode(&self.secret));
if let Some(public) = &self.public {
v.push_str(&hex::encode(public));
}
v
}
pub fn from_x25519_bytes(secret: &[u8]) -> Self {
Self::from_bytes(KeychainStaticSecretAlgorithm::X25519, secret, None)
}
pub fn from_kyber1024_bytes(secret: &[u8], public: &[u8]) -> Self {
Self::from_bytes(KeychainStaticSecretAlgorithm::Kyber1024, secret, Some(public))
}
pub fn from_bytes(algo: KeychainStaticSecretAlgorithm, secret: &[u8], public: Option<&[u8]>) -> Self {
Self {
algo,
secret: secret.to_vec(),
public: public.map(|p| p.to_vec()),
}
}
pub fn to_kyber1204_static_secret(&self) -> XResult<(Kyber1024SecretKey, Kyber1024PublicKey)> {
use pqcrypto_traits::kem::{PublicKey, SecretKey};
let secret_key = opt_result!(Kyber1024SecretKey::from_bytes(&self.secret),
"Parse kyber1204 private key failed: {}");
let public_key = opt_result!(match &self.public {
None => return simple_error!("Kyber1204 public key not found."),
Some(public) => Kyber1024PublicKey::from_bytes(public),
}, "Parse kyber1204 public key failed: {}");
Ok((secret_key, public_key))
}
pub fn to_x25519_static_secret(&self) -> XResult<StaticSecret> {
let secret_slice = self.secret.as_slice();
let mut inner_secret: [u8; 32] = opt_result!(secret_slice.try_into(), "X25519 secret key error: {}");
let static_secret = StaticSecret::from(inner_secret);
inner_secret.zeroize();
Ok(static_secret)
}
pub fn to_x25519_public_key(&self) -> XResult<PublicKey> {
let static_secret = self.to_x25519_static_secret()?;
let public_key: PublicKey = (&static_secret).into();
Ok(public_key)
}
}
impl KeychainKey {
pub fn from(keychain_name: &str, service_name: &str, key_name: &str) -> Self {
debugging!("Keychain key: {} - {} - {}", keychain_name, service_name, key_name);
Self {
keychain_name: keychain_name.to_string(),
service_name: service_name.to_string(),
key_name: key_name.to_string(),
}
}
pub fn parse(keychain_key: &str) -> XResult<Self> {
if !keychain_key.starts_with(KEYCHAIN_KEY_PREFIX) {
return simple_error!("Not a valid keychain key: {}", keychain_key);
}
//keychain:keychain_name:service_name:key_name
let keychain_key_parts = keychain_key.split(':').collect::<Vec<_>>();
if keychain_key_parts.len() != 4 {
return simple_error!("Not a valid keychain key: {}", keychain_key);
}
Ok(Self {
keychain_name: keychain_key_parts[1].to_string(),
service_name: keychain_key_parts[2].to_string(),
key_name: keychain_key_parts[3].to_string(),
})
}
pub fn to_str(&self) -> String {
let mut s = String::new();
s.push_str(KEYCHAIN_KEY_PREFIX);
s.push_str(&self.keychain_name);
s.push(':');
s.push_str(&self.service_name);
s.push(':');
s.push_str(&self.key_name);
s
}
pub fn get_password(&self) -> XResult<Option<Vec<u8>>> {
let sec_keychain = self.get_keychain()?;
debugging!("Try find generic password: {}.{}", &self.service_name, &self.key_name);
match sec_keychain.find_generic_password(&self.service_name, &self.key_name) {
Ok((item_password, _keychain_item)) => {
Ok(Some(item_password.as_ref().to_vec()))
}
Err(e) => {
debugging!("Get password: {} failed: {}", &self.to_str(), e);
Ok(None)
}
}
}
pub fn set_password(&self, password: &[u8]) -> XResult<()> {
let sec_keychain = self.get_keychain()?;
if sec_keychain.find_generic_password(&self.service_name, &self.key_name).is_ok() {
return simple_error!("Password {}.{} exists", &self.service_name, &self.key_name);
}
opt_result!(
sec_keychain.set_generic_password(&self.service_name, &self.key_name, password),
"Set password {}.{} error: {}", &self.service_name, &self.key_name
);
Ok(())
}
fn get_keychain(&self) -> XResult<SecKeychain> {
if !self.keychain_name.is_empty() {
let keychain_file_name = format!("{}.keychain", &self.keychain_name);
debugging!("Open or create keychain: {}", &keychain_file_name);
let keychain_exists = check_keychain_exists(&keychain_file_name);
if keychain_exists {
Ok(opt_result!(SecKeychain::open(&keychain_file_name), "Open keychain: {}, failed: {}", &keychain_file_name))
} else {
match CreateOptions::new().prompt_user(true).create(&keychain_file_name) {
Ok(sec_keychain) => Ok(sec_keychain),
Err(ce) => match SecKeychain::open(&keychain_file_name) {
Ok(sec_keychain) => Ok(sec_keychain),
Err(oe) => simple_error!("Create keychain: {}, failed: {}, open also failed: {}", &self.keychain_name, ce, oe)
}
}
}
} else {
Ok(opt_result!(SecKeychain::default(), "Get keychain failed: {}"))
}
}
}
pub fn decrypt_x25519_data(keychain_key: &KeychainKey, ephemeral_public_key_bytes: &[u8]) -> XResult<Vec<u8>> {
let static_x25519 = opt_value_result!(keychain_key.get_password()?, "Static X25519 not found: {}", &keychain_key.to_str());
let x25519_static_secret = KeychainStaticSecret::parse_bytes(&static_x25519)?;
let static_secret = x25519_static_secret.to_x25519_static_secret()?;
let inner_ephemeral_public_key: [u8; 32] = opt_result!(
ephemeral_public_key_bytes.try_into(), "X25519 public key error: {}");
let ephemeral_public_key = PublicKey::from(inner_ephemeral_public_key);
let shared_secret = static_secret.diffie_hellman(&ephemeral_public_key);
Ok(shared_secret.as_bytes().to_vec())
}
pub fn decrypt_kyber1204_data(keychain_key: &KeychainKey, ephemeral_public_key_bytes: &[u8]) -> XResult<Vec<u8>> {
use pqcrypto_traits::kem::{Ciphertext, SharedSecret};
let static_kyber1204 = opt_value_result!(keychain_key.get_password()?, "Static Kyber1204 not found: {}", &keychain_key.to_str());
let kyber1204_static_secret = KeychainStaticSecret::parse_bytes(&static_kyber1204)?;
let (static_secret, _) = kyber1204_static_secret.to_kyber1204_static_secret()?;
let ciphertext = opt_result!(
Kyber1024Ciphertext::from_bytes(ephemeral_public_key_bytes), "Parse kyber1204 ciphertext failed: {}");
let shared_secret = kyber1024::decapsulate(&ciphertext, &static_secret);
Ok(shared_secret.as_bytes().to_vec())
}
pub fn generate_static_x25519_secret() -> (String, PublicKey) {
let static_secret = StaticSecret::random();
let public_key: PublicKey = (&static_secret).into();
let static_secret_bytes = static_secret.as_bytes();
let x25519_static_secret = KeychainStaticSecret::from_x25519_bytes(static_secret_bytes);
(x25519_static_secret.to_str(), public_key)
}
pub fn generate_static_kyber1024_secret() -> (String, Kyber1024PublicKey) {
use pqcrypto_traits::kem::{PublicKey, SecretKey};
let (public_key, private_key) = kyber1024::keypair();
let static_secret_bytes = private_key.as_bytes();
let static_public_bytes = public_key.as_bytes();
let kyber1024_static_secret =
KeychainStaticSecret::from_kyber1024_bytes(static_secret_bytes, static_public_bytes);
(kyber1024_static_secret.to_str(), public_key)
}
fn check_keychain_exists(keychain_file_name: &str) -> bool {
let keychain_path = PathBuf::from(util_file::resolve_file_path("~/Library/Keychains/"));
match keychain_path.read_dir() {
Ok(read_dir) => {
for dir in read_dir {
match dir {
Ok(dir) => if let Some(file_name) = dir.file_name().to_str() {
if file_name.starts_with(keychain_file_name) {
debugging!("Found key chain file: {:?}", dir);
return true;
}
}
Err(e) => {
debugging!("Read path sub dir: {:?} failed: {}", keychain_path, e);
}
}
}
}
Err(e) => {
debugging!("Read path: {:?} failed: {}", keychain_path, e);
}
}
false
}

25
src/util_log.rs Normal file
View File

@@ -0,0 +1,25 @@
use rust_util::{util_env, util_msg, util_time};
use std::fs::File;
use std::io::Write;
use std::sync::mpsc::channel;
use std::{env, thread};
pub fn init_tiny_encrypt_log() {
if let Some(file_log) = util_env::env_var("TINY_ENCRYPT_FILE_LOG") {
let log_file_name = format!("{}-{}.log", file_log, util_time::get_current_millis());
if let Ok(mut log_file) = File::create(&log_file_name) {
env::set_var("LOGGER_LEVEL", "*"); // set logger to debug
log_file.write_all("Start logging...\n".as_bytes()).ok();
let (sender, receiver) = channel::<String>();
util_msg::set_logger_sender(sender);
thread::spawn(move || loop {
let m = match receiver.recv() {
Ok(msg) => format!("{}\n", msg),
Err(e) => format!("{}\n", e),
};
log_file.write_all(m.as_bytes()).ok();
});
}
}
}

49
src/util_mlkem.rs Normal file
View File

@@ -0,0 +1,49 @@
use ml_kem::kem::Encapsulate;
use ml_kem::{Encoded, EncodedSizeUser, KemCore, MlKem1024, MlKem768};
use rust_util::{opt_result, simple_error, XResult};
#[derive(Clone, Copy, Debug)]
pub enum MlKemAlgo {
MlKem768,
MlKem1024,
}
pub fn ml_kem_768_encapsulate(public_key: &[u8]) -> XResult<(Vec<u8>, Vec<u8>)> {
let encapsulation_key_encoded: Encoded<<MlKem768 as KemCore>::EncapsulationKey> = opt_result!(
public_key.try_into(),
"Parse ML-KEM 768 encapsulation key failed: {}"
);
let encapsulation_key =
<MlKem768 as KemCore>::EncapsulationKey::from_bytes(&encapsulation_key_encoded);
let mut rng = rand::rngs::OsRng;
let (ciphertext, shared_key) = opt_result!(
encapsulation_key.encapsulate(&mut rng),
"Encapsulate shared key failed: {:?}"
);
Ok((shared_key.0.to_vec(), ciphertext.0.to_vec()))
}
pub fn ml_kem_1024_encapsulate(public_key: &[u8]) -> XResult<(Vec<u8>, Vec<u8>)> {
let encapsulation_key_encoded: Encoded<<MlKem1024 as KemCore>::EncapsulationKey> = opt_result!(
public_key.try_into(),
"Parse ML-KEM 1024 encapsulation key failed: {}"
);
let encapsulation_key =
<MlKem1024 as KemCore>::EncapsulationKey::from_bytes(&encapsulation_key_encoded);
let mut rng = rand::rngs::OsRng;
let (ciphertext, shared_key) = opt_result!(
encapsulation_key.encapsulate(&mut rng),
"Encapsulate shared key failed: {:?}"
);
Ok((shared_key.0.to_vec(), ciphertext.0.to_vec()))
}
pub fn try_ml_kem_encapsulate(public_key: &[u8]) -> XResult<(Vec<u8>, Vec<u8>, MlKemAlgo)> {
if let Ok((shared_key, ciphertext)) = ml_kem_768_encapsulate(public_key) {
return Ok((shared_key, ciphertext, MlKemAlgo::MlKem768));
}
if let Ok((shared_key, ciphertext)) = ml_kem_1024_encapsulate(public_key) {
return Ok((shared_key, ciphertext, MlKemAlgo::MlKem1024));
}
simple_error!("Only supports ML-KEM 768 or ML-KEM 1024.")
}

39
src/util_pgp.rs Normal file
View File

@@ -0,0 +1,39 @@
use openpgp_card::{OpenPgp, OpenPgpTransaction};
use openpgp_card_pcsc::PcscBackend;
use rust_util::{failure, opt_result, opt_value_result, simple_error, success, warning, XResult};
use crate::util;
pub fn read_and_verify_openpgp_pin(trans: &mut OpenPgpTransaction, pin: &Option<String>) -> XResult<()> {
let pin = util::read_pin(pin)?;
if let Err(e) = trans.verify_pw1_user(pin.as_ref()) {
failure!("Verify user pin failed: {}", e);
return simple_error!("User pin verify failed: {}", e);
}
success!("User pin verify success!");
util::zeroize(pin);
Ok(())
}
pub fn get_openpgp() -> XResult<OpenPgp> {
let card = match get_card() {
Err(e) => {
return simple_error!("Get card failed: {}", e);
}
Ok(card) => card
};
Ok(OpenPgp::new(card))
}
pub fn get_card() -> XResult<PcscBackend> {
let card_list = opt_result!(
PcscBackend::cards(None), "Read OpenPGP card list failed: {}"
);
if card_list.is_empty() {
return simple_error!("Cannot find any card");
}
if card_list.len() > 1 {
warning!("Find {} OpenPGP cards, will use first card", card_list.len());
}
Ok(opt_value_result!(card_list.into_iter().next(), "SHOULD NOT HAPPEN, CANNOT FIND ANY CARD"))
}

66
src/util_piv.rs Normal file
View File

@@ -0,0 +1,66 @@
use std::io;
use std::io::Write;
use rust_util::{debugging, information, print_ex, simple_error, XResult};
use yubikey::piv::{RetiredSlotId, SlotId};
use crate::config::TinyEncryptConfig;
pub fn read_piv_slot(config: &Option<TinyEncryptConfig>, kid: &str, slot: &Option<String>, silent: bool) -> XResult<String> {
match slot {
Some(slot) => Ok(slot.to_string()),
None => {
if let Some(config) = config {
if let Some(first_arg) = config.find_first_arg_by_kid(kid) {
if silent {
debugging!("Found kid: {}'s slot: {}", kid, first_arg);
} else {
information!("Found kid: {}'s slot: {}", kid, first_arg);
}
return Ok(first_arg.to_string());
}
}
print_ex!("Input slot(eg 82, 83 ...): ");
io::stdout().flush().ok();
let mut buff = String::new();
let _ = io::stdin().read_line(&mut buff).expect("Read line from stdin");
if buff.trim().is_empty() {
simple_error!("Slot is required, and not inputted")
} else {
Ok(buff.trim().to_string())
}
}
}
}
pub fn get_slot_id(slot: &str) -> XResult<SlotId> {
let slot_lower = slot.to_lowercase();
Ok(match slot_lower.as_str() {
"9a" | "auth" | "authentication" => SlotId::Authentication,
"9c" | "sign" | "signature" => SlotId::Signature,
"9d" | "keym" | "keymanagement" => SlotId::KeyManagement,
"9e" | "card" | "cardauthentication" => SlotId::CardAuthentication,
"r1" | "82" => SlotId::Retired(RetiredSlotId::R1),
"r2" | "83" => SlotId::Retired(RetiredSlotId::R2),
"r3" | "84" => SlotId::Retired(RetiredSlotId::R3),
"r4" | "85" => SlotId::Retired(RetiredSlotId::R4),
"r5" | "86" => SlotId::Retired(RetiredSlotId::R5),
"r6" | "87" => SlotId::Retired(RetiredSlotId::R6),
"r7" | "88" => SlotId::Retired(RetiredSlotId::R7),
"r8" | "89" => SlotId::Retired(RetiredSlotId::R8),
"r9" | "8a" => SlotId::Retired(RetiredSlotId::R9),
"r10" | "8b" => SlotId::Retired(RetiredSlotId::R10),
"r11" | "8c" => SlotId::Retired(RetiredSlotId::R11),
"r12" | "8d" => SlotId::Retired(RetiredSlotId::R12),
"r13" | "8e" => SlotId::Retired(RetiredSlotId::R13),
"r14" | "8f" => SlotId::Retired(RetiredSlotId::R14),
"r15" | "90" => SlotId::Retired(RetiredSlotId::R15),
"r16" | "91" => SlotId::Retired(RetiredSlotId::R16),
"r17" | "92" => SlotId::Retired(RetiredSlotId::R17),
"r18" | "93" => SlotId::Retired(RetiredSlotId::R18),
"r19" | "94" => SlotId::Retired(RetiredSlotId::R19),
"r20" | "95" => SlotId::Retired(RetiredSlotId::R20),
_ => return simple_error!("Unknown slot: {}", slot),
})
}

46
src/util_progress.rs Normal file
View File

@@ -0,0 +1,46 @@
use indicatif::{ProgressBar, ProgressStyle};
use rust_util::{debugging, util_msg};
use crate::util_env;
const PB_PROGRESS: &str = "#-";
const PB_TEMPLATE: &str = "{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {bytes}/{total_bytes} {bytes_per_sec} ({eta})";
pub enum Progress {
NoProgress,
Progress(ProgressBar),
}
impl Progress {
pub fn new(total: u64) -> Self {
let no_progress = util_env::get_no_progress();
let is_atty = util_msg::is_atty();
if no_progress || !is_atty {
debugging!("No progress: [{}, {}]", no_progress, is_atty);
Self::NoProgress
} else {
let progress_bar = ProgressBar::new(total);
progress_bar.set_style(ProgressStyle::default_bar()
.template(PB_TEMPLATE).expect("SHOULD NOT FAIL")
.progress_chars(PB_PROGRESS)
);
Self::Progress(progress_bar)
}
}
pub fn position(&self, position: u64) {
match self {
Progress::NoProgress => {}
Progress::Progress(progress_bar) => progress_bar.set_position(position),
}
}
pub fn finish(&self) {
match self {
Progress::NoProgress => {}
Progress::Progress(progress_bar) => progress_bar.finish_and_clear(),
}
}
}

View File

@@ -4,40 +4,21 @@ use x509_parser::prelude::FromDer;
use x509_parser::public_key::RSAPublicKey; use x509_parser::public_key::RSAPublicKey;
use x509_parser::x509::SubjectPublicKeyInfo; use x509_parser::x509::SubjectPublicKeyInfo;
use crate::util::decode_base64; use crate::util;
/// Parse RSA Subject Public Key Info(SPKI) to Rsa Public Key /// Parse RSA Subject Public Key Info(SPKI) to Rsa Public Key
pub fn parse_spki(pem: &str) -> XResult<RsaPublicKey> { pub fn parse_spki(pem: &str) -> XResult<RsaPublicKey> {
let der = pem_to_der_bytes(pem)?; let der = util::parse_pem(pem)?;
let spki = opt_result!(SubjectPublicKeyInfo::from_der(&der), "Parse SKPI failed: {}").1; let spki = opt_result!(SubjectPublicKeyInfo::from_der(&der), "Parse SKPI failed: {}").1;
let public_key_der = spki.subject_public_key.data; let public_key_der = spki.subject_public_key.data;
let public_key = opt_result!(RSAPublicKey::from_der(&public_key_der), "Parse RSA public key failed: {}").1; let public_key = opt_result!(RSAPublicKey::from_der(&public_key_der), "Parse RSA public key failed: {}").1;
let rsa_public_key = opt_result!(RsaPublicKey::new( let rsa_public_key = opt_result!(RsaPublicKey::new(
BigUint::from_bytes_be(public_key.modulus), BigUint::from_bytes_be(public_key.modulus),
BigUint::from_bytes_be(public_key.exponent), BigUint::from_bytes_be(public_key.exponent),
), "Parse Rsa public key failed: {}"); ), "Parse RSA public key failed: {}");
Ok(rsa_public_key) Ok(rsa_public_key)
} }
fn pem_to_der_bytes(pem: &str) -> XResult<Vec<u8>> {
let mut pem = pem.trim().to_owned();
if pem.starts_with("-----BEGIN") {
let mut filter_lines = vec![];
let lines = pem.lines().skip(1);
for ln in lines {
if ln.starts_with("-----END") {
break;
} else {
filter_lines.push(ln.to_string());
}
}
pem = filter_lines.join("");
}
pem = pem.chars().filter(|c| *c != '\n' && *c != '\r').clone().collect::<String>();
Ok(opt_result!(decode_base64(&pem), "Decode pem or der failed: {}"))
}
#[test] #[test]
fn test_parse_spki() { fn test_parse_spki() {
use rsa::traits::PublicKeyParts; use rsa::traits::PublicKeyParts;
@@ -110,7 +91,7 @@ qaCoQsuRtnowGKzrbVdinukd1wj0LkBuz2oNMB3qsXyq7QtOxiFTuKkMOoQNUiCE
KQIDAQAB KQIDAQAB
-----END PUBLIC KEY-----"; -----END PUBLIC KEY-----";
let public_key = parse_spki(public_key_pem).unwrap(); let public_key = parse_spki(public_key_pem).unwrap();
let private_key_der = pem_to_der_bytes(&private_key_pem).unwrap(); let private_key_der = util::parse_pem(&private_key_pem).unwrap();
let private_key_info = PrivateKeyInfo::from_der(&private_key_der).unwrap(); let private_key_info = PrivateKeyInfo::from_der(&private_key_der).unwrap();
let private_key = RsaPrivateKey::try_from(private_key_info).unwrap(); let private_key = RsaPrivateKey::try_from(private_key_info).unwrap();
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();

175
src/util_simple_pbe.rs Normal file
View File

@@ -0,0 +1,175 @@
use crate::util_digest;
use aes_gcm_stream::{Aes256GcmStreamDecryptor, Aes256GcmStreamEncryptor};
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine;
use rand::random;
use rust_util::{opt_result, simple_error, SimpleError, XResult};
use std::fmt::Display;
const SIMPLE_PBKDF_ENCRYPTION_PREFIX: &str = "tinyencrypt-pbkdf-encryption-v1";
// FORMAT
// <PREFIX>.<repeation>.<iterations>.<base64_uri(salt)>.<base64_uri(nonce)>.<base64_uri(ciphertext)>.<base64_uri(tag)>
pub struct SimplePbkdfEncryptionV1 {
pub repetition: u32,
pub iterations: u32,
pub salt: Vec<u8>,
pub nonce: Vec<u8>,
pub ciphertext: Vec<u8>,
pub tag: Vec<u8>,
}
impl SimplePbkdfEncryptionV1 {
pub fn matches(enc: &str) -> bool {
enc.starts_with(&format!("{SIMPLE_PBKDF_ENCRYPTION_PREFIX}."))
}
pub fn encrypt(password: &str, plaintext: &[u8], iterations: &Option<u32>) -> XResult<SimplePbkdfEncryptionV1> {
let salt: [u8; 12] = random();
let repetition = 1000;
let iterations = iterations.unwrap_or(10000);
let key = simple_pbkdf(password.as_bytes(), &salt, repetition, iterations);
let key_bytes: [u8; 32] = opt_result!(key.try_into(), "Bad AES 256 key: {:?}");
let nonce: [u8; 12] = random();
let mut ciphertext = vec![];
let mut aes256_gcm_stream_encryptor = Aes256GcmStreamEncryptor::new(key_bytes, &nonce);
ciphertext.extend_from_slice(&aes256_gcm_stream_encryptor.update(plaintext));
let (last_ciphertext, tag) = aes256_gcm_stream_encryptor.finalize();
ciphertext.extend_from_slice(&last_ciphertext);
Ok(SimplePbkdfEncryptionV1 {
repetition,
iterations,
salt: salt.to_vec(),
nonce: nonce.to_vec(),
ciphertext,
tag,
})
}
pub fn decrypt(&self, password: &str) -> XResult<Vec<u8>> {
let key = simple_pbkdf(
password.as_bytes(),
&self.salt,
self.repetition,
self.iterations,
);
let key_bytes: [u8; 32] = opt_result!(key.try_into(), "Bad AES 256 key: {:?}");
let mut plaintext = vec![];
let mut aes256_gcm_stream_decryptor = Aes256GcmStreamDecryptor::new(key_bytes, &self.nonce);
plaintext.extend_from_slice(&aes256_gcm_stream_decryptor.update(&self.ciphertext));
plaintext.extend_from_slice(&aes256_gcm_stream_decryptor.update(&self.tag));
plaintext.extend_from_slice(&opt_result!(
aes256_gcm_stream_decryptor.finalize(),
"Decrypt failed: {}"
));
Ok(plaintext)
}
}
impl TryFrom<String> for SimplePbkdfEncryptionV1 {
type Error = SimpleError;
fn try_from(enc: String) -> Result<Self, Self::Error> {
TryFrom::<&str>::try_from(enc.as_str())
}
}
impl TryFrom<&str> for SimplePbkdfEncryptionV1 {
type Error = SimpleError;
fn try_from(enc: &str) -> Result<Self, Self::Error> {
if !Self::matches(enc) {
return simple_error!("Not simple PBKDF encryption: {enc}");
}
let parts = enc.split(".").collect::<Vec<_>>();
let repetition: u32 = opt_result!(
parts[1].parse(),
"Parse simple PBKDF failed, invalid repetition: {}, error: {}",
parts[1]
);
let iterations: u32 = opt_result!(
parts[2].parse(),
"Parse simple PBKDF failed, invalid iterations: {}, error: {}",
parts[2]
);
let salt = opt_result!(
URL_SAFE_NO_PAD.decode(parts[3]),
"Parse simple PBKDF failed, invalid salt: {}, error: {}",
parts[3]
);
let nonce = opt_result!(
URL_SAFE_NO_PAD.decode(parts[4]),
"Parse simple PBKDF failed, invalid nonce: {}, error: {}",
parts[4]
);
let ciphertext = opt_result!(
URL_SAFE_NO_PAD.decode(parts[5]),
"Parse simple PBKDF failed, invalid ciphertext: {}, error: {}",
parts[5]
);
let tag = opt_result!(
URL_SAFE_NO_PAD.decode(parts[6]),
"Parse simple PBKDF failed, invalid tag: {}, error: {}",
parts[6]
);
Ok(Self {
repetition,
iterations,
salt,
nonce,
ciphertext,
tag,
})
}
}
impl Display for SimplePbkdfEncryptionV1 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut enc = String::with_capacity(1024);
enc.push_str(SIMPLE_PBKDF_ENCRYPTION_PREFIX);
enc.push('.');
enc.push_str(&self.repetition.to_string());
enc.push('.');
enc.push_str(&self.iterations.to_string());
enc.push('.');
enc.push_str(&URL_SAFE_NO_PAD.encode(&self.salt));
enc.push('.');
enc.push_str(&URL_SAFE_NO_PAD.encode(&self.nonce));
enc.push('.');
enc.push_str(&URL_SAFE_NO_PAD.encode(&self.ciphertext));
enc.push('.');
enc.push_str(&URL_SAFE_NO_PAD.encode(&self.tag));
write!(f, "{}", enc)
}
}
fn simple_pbkdf(password: &[u8], salt: &[u8], repetition: u32, iterations: u32) -> Vec<u8> {
let mut input = password.to_vec();
for it in 0..iterations {
let mut message = Vec::with_capacity((input.len() + salt.len() + 4) * repetition as usize);
for _ in 0..repetition {
message.extend_from_slice(&it.to_be_bytes());
message.extend_from_slice(&input);
message.extend_from_slice(salt);
}
input = util_digest::sha256_digest(&message);
}
input
}
#[test]
fn test() {
let enc = SimplePbkdfEncryptionV1::encrypt("helloworld", "test".as_bytes(), &None).unwrap();
let enc_str = enc.to_string();
let enc2: SimplePbkdfEncryptionV1 = enc_str.try_into().unwrap();
assert_eq!(enc.to_string(), enc2.to_string());
let plain = enc2.decrypt("helloworld").unwrap();
assert_eq!(b"test", plain.as_slice());
}

View File

@@ -1,19 +0,0 @@
use rand::rngs::OsRng;
use rust_util::{opt_result, simple_error, XResult};
use x25519_dalek::{EphemeralSecret, PublicKey};
pub fn compute_x25519_shared_secret(public_key_point_hex: &str) -> XResult<(Vec<u8>, Vec<u8>)> {
let public_key_bytes = opt_result!(hex::decode(public_key_point_hex), "Parse X25519 public key hex failed: {}");
if public_key_bytes.len() != 32 {
return simple_error!("Parse X25519 key failed: not 32 bytes");
}
let public_key_bytes: [u8; 32] = public_key_bytes.try_into().unwrap();
let public_key_card = PublicKey::from(public_key_bytes);
let ephemeral_secret = EphemeralSecret::random_from_rng(OsRng);
let ephemeral_public = PublicKey::from(&ephemeral_secret);
let shared_secret = ephemeral_secret.diffie_hellman(&public_key_card);
Ok((shared_secret.as_bytes().to_vec(), ephemeral_public.as_bytes().to_vec()))
}

View File

@@ -1,7 +1,9 @@
use rust_util::{opt_result, simple_error, XResult}; use rust_util::{opt_result, simple_error, XResult};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::util::{decode_base64_url_no_pad, encode_base64_url_no_pad}; use crate::util;
pub const WRAP_KEY_PREFIX: &str = "WK:";
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
@@ -14,41 +16,58 @@ pub struct WrapKey {
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct WrapKeyHeader { pub struct WrapKeyHeader {
#[serde(skip_serializing_if = "Option::is_none")]
pub kid: Option<String>, pub kid: Option<String>,
pub enc: String, pub enc: String,
pub e_pub_key: String, pub e_pub_key: String,
} }
impl WrapKeyHeader {
pub fn from(enc_type: &str, ephemeral_spki: &[u8]) -> Self {
WrapKeyHeader {
kid: None,
enc: enc_type.to_string(),
e_pub_key: util::encode_base64_url_no_pad(ephemeral_spki),
}
}
pub fn get_e_pub_key_bytes(self) -> XResult<Vec<u8>> {
Ok(opt_result!(util::decode_base64_url_no_pad(&self.e_pub_key), "Invalid envelop e_pub_key: {}"))
}
}
impl WrapKey { impl WrapKey {
pub fn encode(&self) -> XResult<String> { pub fn encode(&self) -> XResult<String> {
let mut buf = String::with_capacity(512); let mut buf = String::with_capacity(512);
buf.push_str("WK:"); buf.push_str(WRAP_KEY_PREFIX);
let header = serde_json::to_string(&self.header)?; let header = serde_json::to_string(&self.header)?;
let header_str = encode_base64_url_no_pad(header.as_bytes()); let header_str = util::encode_base64_url_no_pad(header.as_bytes());
let nonce_str = util::encode_base64_url_no_pad(&self.nonce);
let encrypted_data_str = util::encode_base64_url_no_pad(&self.encrypted_data);
buf.push_str(&header_str); buf.push_str(&header_str);
buf.push('.'); buf.push('.');
buf.push_str(&encode_base64_url_no_pad(&self.nonce)); buf.push_str(&nonce_str);
buf.push('.'); buf.push('.');
buf.push_str(&encode_base64_url_no_pad(&self.encrypted_data)); buf.push_str(&encrypted_data_str);
Ok(buf) Ok(buf)
} }
pub fn parse(wk: &str) -> XResult<WrapKey> { pub fn parse(wk: &str) -> XResult<WrapKey> {
if !wk.starts_with("WK:") { if !wk.starts_with(WRAP_KEY_PREFIX) {
return simple_error!("Wrap key string must starts with WK:"); return simple_error!("Wrap key string must starts with WK:");
} }
let wks = wk.split(".").collect::<Vec<_>>(); let wks = wk.split('.').collect::<Vec<_>>();
if wks.len() != 3 { if wks.len() != 3 {
return simple_error!("Invalid wrap key."); return simple_error!("Invalid wrap key.");
} }
let header = wks[0].chars().skip(3).collect::<String>(); let header = wks[0].chars().skip(3).collect::<String>();
let header_bytes = opt_result!(decode_base64_url_no_pad(&header), "Invalid wrap key header: {}"); let header_bytes = opt_result!(util::decode_base64_url_no_pad(&header), "Invalid wrap key header: {}");
let nonce = wks[1]; let nonce = wks[1];
let encrypted_data = wks[2]; let encrypted_data = wks[2];
let header_str = opt_result!(String::from_utf8(header_bytes), "Invalid wrap key header: {}"); let header_str = opt_result!(String::from_utf8(header_bytes), "Invalid wrap key header: {}");
let header: WrapKeyHeader = opt_result!(serde_json::from_str(&header_str), "Invalid wrap key header: {}"); let header: WrapKeyHeader = opt_result!(serde_json::from_str(&header_str), "Invalid wrap key header: {}");
let nonce = opt_result!(decode_base64_url_no_pad(nonce), "Invalid wrap key: {}"); let nonce = opt_result!(util::decode_base64_url_no_pad(nonce), "Invalid wrap key: {}");
let encrypted_data = opt_result!(decode_base64_url_no_pad(encrypted_data), "Invalid wrap key: {}"); let encrypted_data = opt_result!(util::decode_base64_url_no_pad(encrypted_data), "Invalid wrap key: {}");
Ok(WrapKey { Ok(WrapKey {
header, header,
nonce, nonce,