Compare commits
242 Commits
12aa3b48d2
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
0604745f82
|
|||
|
1f4db9d1b0
|
|||
|
cdec79e4dc
|
|||
|
3812001474
|
|||
| 3708781390 | |||
|
a6397fc45a
|
|||
| 0e6b590f32 | |||
|
b0ee1f2c59
|
|||
|
c176021c81
|
|||
|
d75c589b66
|
|||
|
0c4663f7f0
|
|||
|
c446a52462
|
|||
|
9b0ecef9a0
|
|||
|
783b3e1962
|
|||
|
7b7878e2c1
|
|||
|
403eaf1669
|
|||
|
75ed193d86
|
|||
|
c8175e2654
|
|||
|
7c752eab41
|
|||
|
96ebf7f592
|
|||
|
b18a5ec3e2
|
|||
|
813b8b1b24
|
|||
|
6a84ae7f5c
|
|||
|
ff30fd42dd
|
|||
|
0ad8f83092
|
|||
|
1741c335db
|
|||
|
93cb3309bf
|
|||
|
6a24388a19
|
|||
|
b94acf9c31
|
|||
|
b91b29e22d
|
|||
|
12e828732e
|
|||
|
d98005d799
|
|||
|
e308809b20
|
|||
|
920aa92b0e
|
|||
|
20c54350ee
|
|||
|
044daaad7d
|
|||
|
f0f505bde3
|
|||
|
4ad735d840
|
|||
|
d831b606cd
|
|||
| 84ee9d1b34 | |||
|
1b10c2a0c8
|
|||
|
3c717a8f8d
|
|||
|
a7802a3750
|
|||
|
cc5178d1c1
|
|||
|
b21714cb4b
|
|||
|
6635a0e468
|
|||
|
8b38982e61
|
|||
|
528889bcc7
|
|||
|
338017590f
|
|||
|
08fa61c968
|
|||
|
75740a26a4
|
|||
|
85a7448291
|
|||
|
cd9b3eb624
|
|||
|
0aafaa7586
|
|||
|
408b3e468b
|
|||
|
fd0751d348
|
|||
|
096218fc78
|
|||
|
2a03dd45ce
|
|||
|
3046626691
|
|||
|
f2388b11cf
|
|||
|
90372941be
|
|||
| b9e7b5af63 | |||
|
8e6e708c73
|
|||
|
6a07360dc1
|
|||
|
d6e1f96207
|
|||
|
228c69731a
|
|||
|
568022e655
|
|||
|
ac727f082e
|
|||
|
623e657211
|
|||
|
598dbc38c7
|
|||
|
f74a81d4ca
|
|||
|
0309f77ed7
|
|||
|
1340f73bad
|
|||
|
2067faec8e
|
|||
|
de675d215a
|
|||
|
3127dfab0a
|
|||
|
0022181bd6
|
|||
|
af1678f67a
|
|||
|
80ce9fcece
|
|||
|
62feb2e246
|
|||
|
e31f6d49b7
|
|||
|
784765fbb9
|
|||
|
6283c28fff
|
|||
|
c093f865ab
|
|||
|
27c6bee4ea
|
|||
|
2bf0614854
|
|||
|
2bcb0fe5c4
|
|||
|
2e53130ead
|
|||
|
3ce150db72
|
|||
|
3e2a3cc7f3
|
|||
|
61853889ca
|
|||
|
b7d3f77385
|
|||
|
004df06df2
|
|||
|
e60f491801
|
|||
|
5f04aa5783
|
|||
|
0c5dbc7cc3
|
|||
|
d19c9a48f1
|
|||
|
def76d1957
|
|||
|
171cd2d2a4
|
|||
|
a3d101a405
|
|||
|
9b73b4ecc1
|
|||
|
e7b4f022bf
|
|||
|
df5ba3c53d
|
|||
|
0b09630635
|
|||
|
343a6f5fac
|
|||
|
a7398a2578
|
|||
|
9de9af0cde
|
|||
|
5bdc4c69e6
|
|||
|
c30d061f53
|
|||
|
bafb12bdf7
|
|||
|
5822cf9b7f
|
|||
|
7f1abf7dea
|
|||
|
f9a5f39330
|
|||
|
fc3275d978
|
|||
|
26c5a25759
|
|||
|
e7a9877cad
|
|||
|
fd98c27462
|
|||
|
0189b5a765
|
|||
|
90ecfe76ba
|
|||
|
2c2f623df4
|
|||
|
c597a87557
|
|||
|
2ccb4f213b
|
|||
|
33e08e657d
|
|||
|
880b5be94e
|
|||
|
7ee38b8ac5
|
|||
|
c393302d6c
|
|||
|
72d89b32ab
|
|||
|
c1debe5262
|
|||
|
54ac065bd3
|
|||
|
f5bd348d5b
|
|||
|
c49dfcf261
|
|||
|
d0218ee233
|
|||
|
b15b9a5b32
|
|||
|
2845e5155e
|
|||
|
34b60f6929
|
|||
|
68d3c07767
|
|||
|
0a815dbbdf
|
|||
|
993f10ba71
|
|||
|
af7bf60869
|
|||
|
9b735f8e48
|
|||
|
2d29e6eaed
|
|||
|
baafa7aa31
|
|||
|
df36022203
|
|||
|
5e73466400
|
|||
|
115e0f7094
|
|||
|
d75a88c90c
|
|||
|
36fcaab6be
|
|||
|
42bc09fe07
|
|||
|
a8b2fc62b8
|
|||
|
2034db589a
|
|||
|
fa1cd80fc8
|
|||
|
215b75d6da
|
|||
|
d36c06f461
|
|||
|
6b132b800a
|
|||
|
a60bf5c74a
|
|||
|
43b43ba055
|
|||
|
be65cfb06c
|
|||
|
24c6d3bbad
|
|||
|
638e40448a
|
|||
|
bd586130fb
|
|||
|
bae6c13582
|
|||
|
92e6808820
|
|||
|
fd7e8d35a6
|
|||
|
883ed0918b
|
|||
|
fc4ec8d41c
|
|||
|
76b8f93ddd
|
|||
|
0bf4ef2e6b
|
|||
|
9e36725908
|
|||
|
1e2f13e0b8
|
|||
|
ae84bbd13b
|
|||
|
4966ddf72b
|
|||
|
4b426f7b13
|
|||
|
9c5b28eb2a
|
|||
|
4306825706
|
|||
|
42a7a0fb28
|
|||
|
5ec12237d9
|
|||
|
699474cad4
|
|||
|
e6b1ec8a86
|
|||
|
6279e8e16b
|
|||
|
d8ae0ad9db
|
|||
|
3a3ebd64bf
|
|||
|
25de8ea682
|
|||
|
9e46d11e89
|
|||
|
fa5d01a7d5
|
|||
|
a6d78928d9
|
|||
|
e478e43e12
|
|||
|
231bf28240
|
|||
|
8aec9ad9a3
|
|||
|
6161e2d293
|
|||
|
67465d584e
|
|||
|
e355d0f03d
|
|||
|
9d3de9c8f0
|
|||
|
84eb5c789b
|
|||
|
20b0fe1ea2
|
|||
|
89ed3c26ed
|
|||
|
373b3bcefc
|
|||
|
9ec0dba5b3
|
|||
|
3d59680f0c
|
|||
|
80e3a3540e
|
|||
|
19269bf6de
|
|||
|
c05cf1a7cf
|
|||
|
d5cb25cc6a
|
|||
|
f039c183b2
|
|||
|
dcda185512
|
|||
|
0fa22f7f39
|
|||
|
0f02edc6f6
|
|||
|
2a7d28372e
|
|||
|
5dc1927cc9
|
|||
|
1012b33f31
|
|||
|
bb07aec896
|
|||
|
83464dfb28
|
|||
|
b3359d205b
|
|||
|
a66babb828
|
|||
|
94a6cf18b3
|
|||
|
aa6c4f325d
|
|||
|
452cb2b666
|
|||
|
6e00247bf6
|
|||
|
186330fe2c
|
|||
|
ddd3ac3b2d
|
|||
|
17fae72d91
|
|||
|
e0b6df7c46
|
|||
|
90040bbf8e
|
|||
|
9db0cec508
|
|||
|
b4d9b4692b
|
|||
|
ab2e850435
|
|||
|
f281cc15a2
|
|||
|
7d0115541f
|
|||
|
47a0625dcc
|
|||
|
eda2d94d21
|
|||
|
0ab41c87fb
|
|||
|
cb72baebee
|
|||
|
6f70e7c83e
|
|||
|
bec7306918
|
|||
|
dda040a1b3
|
|||
|
bae6ca828d
|
|||
|
66e93106fe
|
|||
|
a37d5b8d62
|
|||
|
93b957a4c3
|
|||
|
b0af535aa3
|
|||
|
cd359bb6ac
|
|||
|
03ec9a0fe0
|
|||
|
1a3c670bad
|
10
.gitignore
vendored
10
.gitignore
vendored
@@ -1,3 +1,5 @@
|
||||
.swiftpm/
|
||||
.build/
|
||||
_tinyencrypt_config-rs.json
|
||||
*.tinyenc
|
||||
# ---> Rust
|
||||
@@ -7,6 +9,11 @@ debug/
|
||||
target/
|
||||
.idea/
|
||||
|
||||
libse.a
|
||||
se.swiftdoc
|
||||
se.swiftmodule
|
||||
se.swiftsourceinfo
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
@@ -20,7 +27,8 @@ target/
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
Icon
|
||||
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
2249
Cargo.lock
generated
2249
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
48
Cargo.toml
48
Cargo.toml
@@ -1,38 +1,62 @@
|
||||
[package]
|
||||
name = "tiny-encrypt"
|
||||
version = "0.3.3"
|
||||
version = "1.9.20"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
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
|
||||
|
||||
[features]
|
||||
default = ["decrypt", "macos", "smartcard"]
|
||||
full-features = ["decrypt", "macos", "smartcard"]
|
||||
decrypt = ["smartcard"]
|
||||
smartcard = ["openpgp-card", "openpgp-card-pcsc", "yubikey"]
|
||||
macos = ["security-framework"]
|
||||
|
||||
[dependencies]
|
||||
aes-gcm-stream = "0.2"
|
||||
base64 = "0.21"
|
||||
base64 = "0.22"
|
||||
chacha20-poly1305-stream = "0.1"
|
||||
chrono = "0.4"
|
||||
clap = { version = "4.4", features = ["derive"] }
|
||||
flate2 = "1.0"
|
||||
fs-set-times = "0.20"
|
||||
hex = "0.4"
|
||||
openpgp-card = "0.3"
|
||||
openpgp-card-pcsc = "0.3"
|
||||
indicatif = "0.18"
|
||||
openpgp-card = { version = "0.3", optional = true }
|
||||
openpgp-card-pcsc = { version = "0.3", optional = true }
|
||||
p256 = { version = "0.13", features = ["pem", "ecdh", "pkcs8"] }
|
||||
p384 = { version = "0.13", features = ["pem", "ecdh", "pkcs8"] }
|
||||
rand = "0.8"
|
||||
reqwest = { version = "0.11", features = ["blocking", "rustls", "rustls-tls"] }
|
||||
rpassword = "7.2"
|
||||
# reqwest = { version = "0.11", features = ["blocking", "rustls", "rustls-tls"] }
|
||||
rpassword = "7.3"
|
||||
rsa = { version = "0.9", features = ["pem"] }
|
||||
rust-crypto-hatter-fork = "0.2"
|
||||
rust_util = "0.6"
|
||||
security-framework = { version = "3.0", features = ["OSX_10_15"], optional = true }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
sha256 = "1.4"
|
||||
simpledateformat = "0.1"
|
||||
tabled = "0.14"
|
||||
x25519-dalek = "2.0"
|
||||
x509-parser = "0.15"
|
||||
yubikey = { version = "0.8", features = ["untested"] }
|
||||
zeroize = "1.6"
|
||||
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]
|
||||
codegen-units = 1
|
||||
|
||||
124
README.md
124
README.md
@@ -1,11 +1,13 @@
|
||||
# tiny-encrypt-rs
|
||||
|
||||
**IMPORTANT**: To use tiny-encrypt, a Yubikey(https://www.yubico.com/products/) is
|
||||
required, the key MUST support PIV or OpenPGP.
|
||||
> [!IMPORTANT]
|
||||
> 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.
|
||||
|
||||
|
||||

|
||||
|
||||
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)
|
||||
|
||||
@@ -14,21 +16,59 @@ Specification: [Tiny Encrypt Spec V1.1](https://github.com/OpenWebStandard/tiny-
|
||||
|
||||
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>
|
||||
|
||||
Encrypt config `~/.tinyencrypt/config-rs.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"environment": {
|
||||
"TINY_ENCRYPT_DEFAULT_ALGORITHM": "AES or CHACHA20"
|
||||
},
|
||||
"namespaces": {
|
||||
"name": "/Users/example/.name"
|
||||
},
|
||||
"envelops": [
|
||||
{
|
||||
"type": "pgp",
|
||||
"type": "pgp-rsa",
|
||||
"kid": "KID-1",
|
||||
"desc": "this is key 001",
|
||||
"publicPart": "----- BEGIN PUBLIC KEY ..."
|
||||
},
|
||||
{
|
||||
"type": "ecdh",
|
||||
"type": "piv-p256",
|
||||
"kid": "KID-2",
|
||||
"desc": "this is key 002",
|
||||
"publicPart": "04..."
|
||||
@@ -46,16 +86,53 @@ Encrypt config `~/.tinyencrypt/config-rs.json`:
|
||||
}
|
||||
```
|
||||
|
||||
<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 | PKCS1-v1.5 | OpenPGP Encryption Key |
|
||||
| pgp-x25519 | ECDH(X25519) | OpenPGP Encryption Key |
|
||||
| ecdh | ECDH(secp256r1) | PIV Slot |
|
||||
| ecdh-p384 | ECDH(secp384r1) | PIV Slot |
|
||||
| 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:
|
||||
Smart Card(Yubikey) protected ECDH Encryption description as below:
|
||||
|
||||
```text
|
||||
┌───────────────────┐ ┌───────────────────────────┐
|
||||
@@ -78,3 +155,26 @@ Smart Card(Yubikey) protected ECDH Encryption description:
|
||||
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
|
||||
```
|
||||
5
USAGE.md
5
USAGE.md
@@ -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
|
||||
```
|
||||
|
||||
## 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
34
justfile
Normal 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
|
||||
15
src/card.rs
15
src/card.rs
@@ -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"))
|
||||
}
|
||||
@@ -2,12 +2,13 @@ use std::cmp::Ordering;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use clap::Args;
|
||||
use rust_util::XResult;
|
||||
use rust_util::{iff, information, warning, XResult};
|
||||
use tabled::{Table, Tabled};
|
||||
use tabled::settings::Style;
|
||||
|
||||
use crate::config::TinyEncryptConfig;
|
||||
use crate::consts::TINY_ENC_CONFIG_FILE;
|
||||
use crate::temporary_key::serialize_config_envelop;
|
||||
use crate::util_envelop;
|
||||
|
||||
#[derive(Tabled, Eq)]
|
||||
struct ConfigProfile {
|
||||
@@ -29,25 +30,119 @@ impl Ord for ConfigProfile {
|
||||
|
||||
impl PartialOrd for ConfigProfile {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
self.profiles.partial_cmp(&other.profiles)
|
||||
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 {}
|
||||
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_version: CmdConfig) -> XResult<()> {
|
||||
let config = TinyEncryptConfig::load(TINY_ENC_CONFIG_FILE)?;
|
||||
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();
|
||||
for (p, v) in &config.profiles {
|
||||
let p = p;
|
||||
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)); }
|
||||
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)); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +151,9 @@ pub fn config(_cmd_version: CmdConfig) -> XResult<()> {
|
||||
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 {
|
||||
@@ -64,10 +162,20 @@ pub fn config(_cmd_version: CmdConfig) -> XResult<()> {
|
||||
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!("{}{}", envelop.kid, desc));
|
||||
ks.push(format!(
|
||||
"{}, {}{}",
|
||||
util_envelop::with_width_type(envelop.r#type.get_name()), kid, desc
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -83,4 +191,4 @@ pub fn config(_cmd_version: CmdConfig) -> XResult<()> {
|
||||
println!("{}", table);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
125
src/cmd_directdecrypt.rs
Normal file
125
src/cmd_directdecrypt.rs
Normal 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)
|
||||
}
|
||||
@@ -7,55 +7,99 @@ use std::time::Instant;
|
||||
use clap::Args;
|
||||
use flate2::Compression;
|
||||
use rsa::Pkcs1v15Encrypt;
|
||||
use rust_util::{debugging, failure, iff, information, opt_result, simple_error, success, util_msg, 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::{file, util, util_ecdh, util_p384, util_x25519};
|
||||
use crate::compress::GzStreamEncoder;
|
||||
use crate::config::{TinyEncryptConfig, TinyEncryptConfigEnvelop};
|
||||
use crate::consts::{ENC_AES256_GCM_P256, ENC_AES256_GCM_P384, ENC_AES256_GCM_X25519, SALT_COMMENT, TINY_ENC_CONFIG_FILE, TINY_ENC_FILE_EXT};
|
||||
use crate::crypto_aes::{aes_gcm_encrypt, aes_gcm_encrypt_with_salt};
|
||||
use crate::crypto_rsa::parse_spki;
|
||||
use crate::spec::{EncEncryptedMeta, EncMetadata, TINY_ENCRYPT_VERSION_10, TinyEncryptEnvelop, TinyEncryptEnvelopType, TinyEncryptMeta};
|
||||
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_cryptor::{Cryptor, KeyNonce};
|
||||
use crate::spec::{
|
||||
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::{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)]
|
||||
pub struct CmdEncrypt {
|
||||
/// Files need to be decrypted
|
||||
pub paths: Vec<PathBuf>,
|
||||
/// Comment
|
||||
/// Plaintext comment
|
||||
#[arg(long, short = 'c')]
|
||||
pub comment: Option<String>,
|
||||
|
||||
/// Encrypted comment
|
||||
#[arg(long, short = 'C')]
|
||||
pub encrypted_comment: Option<String>,
|
||||
/// Encryption profile
|
||||
|
||||
/// 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>>,
|
||||
|
||||
/// Compress before encrypt
|
||||
#[arg(long, short = 'x')]
|
||||
pub compress: bool,
|
||||
|
||||
/// Compress level (from 0[none], 1[fast] .. 6[default] .. to 9[best])
|
||||
#[arg(long, short = 'L')]
|
||||
pub compress_level: Option<u32>,
|
||||
/// Compatible with 1.0
|
||||
#[arg(long, short = '1')]
|
||||
pub compatible_with_1_0: bool,
|
||||
|
||||
/// Remove source file
|
||||
#[arg(long, short = 'R')]
|
||||
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<()> {
|
||||
let config = TinyEncryptConfig::load(TINY_ENC_CONFIG_FILE)?;
|
||||
let config = TinyEncryptConfig::load_default(&cmd_encrypt.config)?;
|
||||
debugging!("Found tiny encrypt config: {:?}", config);
|
||||
let envelops = config.find_envelops(&cmd_encrypt.profile)?;
|
||||
if envelops.is_empty() { return simple_error!("Cannot find any valid envelops"); }
|
||||
let mut envelops = config.find_envelops(&cmd_encrypt.profile, &cmd_encrypt.key_filter)?;
|
||||
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()
|
||||
.map(|e| format!("{}:{}", e.r#type.get_name(), e.kid))
|
||||
.collect();
|
||||
@@ -68,8 +112,9 @@ pub fn encrypt(cmd_encrypt: CmdEncrypt) -> XResult<()> {
|
||||
let mut failed_count = 0;
|
||||
let mut total_len = 0_u64;
|
||||
for path in &cmd_encrypt.paths {
|
||||
let path = config.resolve_path_namespace(path, false);
|
||||
let start_encrypt_single = Instant::now();
|
||||
match encrypt_single(path, &envelops, &cmd_encrypt) {
|
||||
match encrypt_single(&path, &envelops, &cmd_encrypt) {
|
||||
Ok(len) => {
|
||||
total_len += len;
|
||||
if len > 0 { succeed_count += 1; } else { skipped_count += 1; }
|
||||
@@ -99,27 +144,55 @@ pub fn encrypt(cmd_encrypt: CmdEncrypt) -> XResult<()> {
|
||||
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());
|
||||
if path_display.ends_with(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);
|
||||
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 path_out = format!("{}{}", path_display, TINY_ENC_FILE_EXT);
|
||||
util::require_file_not_exists(path_out.as_str())?;
|
||||
util::require_file_not_exists(path_out)?;
|
||||
|
||||
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 {
|
||||
None => None,
|
||||
Some(encrypted_comment) => Some(util::encode_base64(
|
||||
&aes_gcm_encrypt_with_salt(&key, &nonce, SALT_COMMENT, 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());
|
||||
@@ -128,132 +201,144 @@ fn encrypt_single(path: &PathBuf, envelops: &[&TinyEncryptConfigEnvelop], cmd_en
|
||||
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(&key, &nonce), "Seal enc-encrypted-meta failed: {}");
|
||||
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 {
|
||||
comment: cmd_encrypt.comment.clone(),
|
||||
encrypted_comment,
|
||||
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);
|
||||
|
||||
if cmd_encrypt.compatible_with_1_0 {
|
||||
encrypt_meta = process_compatible_with_1_0(cmd_encrypt, encrypt_meta)?;
|
||||
let start = Instant::now();
|
||||
|
||||
let mut file_out = File::create(path_out)?;
|
||||
let compress_meta = !cmd_encrypt.disable_compress_meta;
|
||||
|
||||
if cmd_encrypt.pem_output {
|
||||
let temp_output_len = file_in.metadata().map(|m| m.len()).unwrap_or(0) + 1024 * 8;
|
||||
if temp_output_len > 8 * 1024 * 1028 {
|
||||
warning!("Input file is more than 8 MiB.");
|
||||
}
|
||||
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,
|
||||
)?;
|
||||
}
|
||||
|
||||
let mut file_out = File::create(&path_out)?;
|
||||
let compress_meta = !cmd_encrypt.disable_compress_meta;
|
||||
let _ = file::write_tiny_encrypt_meta(&mut file_out, &encrypt_meta, compress_meta)?;
|
||||
|
||||
let compress_desc = iff!(cmd_encrypt.compress, " [with compress]", "");
|
||||
let start = Instant::now();
|
||||
util_msg::print_lastline(
|
||||
&format!("Encrypting file: {}{} ...", path_display, compress_desc)
|
||||
);
|
||||
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!("Inner encrypt file{}: {} elapsed: {} ms", compress_desc, path_display, encrypt_duration.as_millis());
|
||||
|
||||
util::zeroize(key);
|
||||
util::zeroize(nonce);
|
||||
drop(file_in);
|
||||
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())
|
||||
}
|
||||
|
||||
fn process_compatible_with_1_0(cmd_encrypt: &CmdEncrypt, mut encrypt_meta: TinyEncryptMeta) -> XResult<TinyEncryptMeta> {
|
||||
if !cmd_encrypt.disable_compress_meta {
|
||||
return simple_error!("Compatible with 1.0 mode must turns --disable-compress-meta on.");
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
Ok(encrypt_meta)
|
||||
}
|
||||
|
||||
fn encrypt_file(file_in: &mut File, file_out: &mut File, key: &[u8], nonce: &[u8], compress: bool, compress_level: &Option<u32>) -> XResult<usize> {
|
||||
let mut total_len = 0;
|
||||
pub(crate) fn encrypt_file(file_in: &mut impl Read, file_len: u64, file_out: &mut impl Write, cryptor: Cryptor,
|
||||
key_nonce: &KeyNonce, compress_level: &Option<u32>) -> XResult<u64> {
|
||||
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 key = opt_result!(key.try_into(), "Key is not 32 bytes: {}");
|
||||
let mut gz_encoder = match compress_level {
|
||||
None => GzStreamEncoder::new_default(),
|
||||
Some(compress_level) => {
|
||||
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))
|
||||
}
|
||||
};
|
||||
let mut encryptor = aes_gcm_stream::Aes256GcmStreamEncryptor::new(key, nonce);
|
||||
let progress = Progress::new(file_len);
|
||||
let mut encryptor = cryptor.encryptor(key_nonce)?;
|
||||
loop {
|
||||
let len = opt_result!(file_in.read(&mut buffer), "Read file failed: {}");
|
||||
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 mut encrypted_block = encryptor.update(&last_compressed_buffer);
|
||||
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(&tag);
|
||||
encrypted_block
|
||||
} else {
|
||||
let (mut last_block, tag) = encryptor.finalize();
|
||||
write_len += last_block.len() as u64;
|
||||
last_block.extend_from_slice(&tag);
|
||||
last_block
|
||||
};
|
||||
opt_result!(file_out.write_all(&last_block), "Write file failed: {}");
|
||||
debugging!("Encrypt finished, total bytes: {}", total_len);
|
||||
opt_result!(file_out.write_all(&last_block_and_tag), "Write file failed: {}");
|
||||
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;
|
||||
} else {
|
||||
total_len += len;
|
||||
total_len += len as u64;
|
||||
let encrypted = if compress {
|
||||
let compressed = opt_result!(gz_encoder.update(&buffer[0..len]), "Decompress file failed: {}");
|
||||
encryptor.update(&compressed)
|
||||
} else {
|
||||
encryptor.update(&buffer[0..len])
|
||||
};
|
||||
write_len += encrypted.len() as u64;
|
||||
opt_result!(file_out.write_all(&encrypted), "Write file failed: {}");
|
||||
progress.position(total_len);
|
||||
}
|
||||
}
|
||||
let mut key = key;
|
||||
key.zeroize();
|
||||
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![];
|
||||
for envelop in envelops {
|
||||
match envelop.r#type {
|
||||
TinyEncryptEnvelopType::Pgp => {
|
||||
encrypted_envelops.push(encrypt_envelop_pgp(key, envelop)?);
|
||||
TinyEncryptEnvelopType::PgpRsa | TinyEncryptEnvelopType::PivRsa => {
|
||||
encrypted_envelops.push(encrypt_envelop_rsa(key, envelop)?);
|
||||
}
|
||||
TinyEncryptEnvelopType::PgpX25519 => {
|
||||
encrypted_envelops.push(encrypt_envelop_ecdh_x25519(key, envelop)?);
|
||||
TinyEncryptEnvelopType::Gpg => {
|
||||
encrypted_envelops.push(encrypt_envelop_gpg(key, envelop)?);
|
||||
}
|
||||
TinyEncryptEnvelopType::Ecdh => {
|
||||
encrypted_envelops.push(encrypt_envelop_ecdh(key, envelop)?);
|
||||
TinyEncryptEnvelopType::PgpX25519 | TinyEncryptEnvelopType::StaticX25519 => {
|
||||
encrypted_envelops.push(encrypt_envelop_ecdh_x25519(cryptor, key, envelop)?);
|
||||
}
|
||||
TinyEncryptEnvelopType::EcdhP384 => {
|
||||
encrypted_envelops.push(encrypt_envelop_ecdh_p384(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),
|
||||
}
|
||||
@@ -261,44 +346,75 @@ fn encrypt_envelops(key: &[u8], envelops: &[&TinyEncryptConfigEnvelop]) -> XResu
|
||||
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 (shared_secret, ephemeral_spki) = util_ecdh::compute_shared_secret(public_key_point_hex)?;
|
||||
|
||||
encrypt_envelop_shared_secret(key, &shared_secret, &ephemeral_spki, ENC_AES256_GCM_P256, envelop)
|
||||
let (shared_secret, ephemeral_spki) = ecdh_p256::compute_p256_shared_secret(public_key_point_hex)?;
|
||||
let enc_type = match cryptor {
|
||||
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_p384(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 (shared_secret, ephemeral_spki) = util_p384::compute_p384_shared_secret(public_key_point_hex)?;
|
||||
|
||||
encrypt_envelop_shared_secret(key, &shared_secret, &ephemeral_spki, ENC_AES256_GCM_P384, envelop)
|
||||
let (shared_secret, ephemeral_spki) = ecdh_p384::compute_p384_shared_secret(public_key_point_hex)?;
|
||||
let enc_type = match cryptor {
|
||||
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_ecdh_x25519(key: &[u8], envelop: &TinyEncryptConfigEnvelop) -> XResult<TinyEncryptEnvelop> {
|
||||
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) = util_x25519::compute_x25519_shared_secret(public_key_point_hex)?;
|
||||
|
||||
encrypt_envelop_shared_secret(key, &shared_secret, &ephemeral_spki, ENC_AES256_GCM_X25519, envelop)
|
||||
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_shared_secret(key: &[u8],
|
||||
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],
|
||||
ephemeral_spki: &[u8],
|
||||
enc_type: &str,
|
||||
envelop: &TinyEncryptConfigEnvelop) -> XResult<TinyEncryptEnvelop> {
|
||||
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 {
|
||||
header: WrapKeyHeader {
|
||||
kid: None, // Some(envelop.kid.clone()),
|
||||
enc: enc_type.to_string(),
|
||||
e_pub_key: util::encode_base64_url_no_pad(ephemeral_spki),
|
||||
},
|
||||
nonce,
|
||||
header: WrapKeyHeader::from(enc_type, ephemeral_spki),
|
||||
nonce: nonce.0.clone(),
|
||||
encrypted_data: encrypted_key,
|
||||
};
|
||||
let encoded_wrap_key = wrap_key.encode()?;
|
||||
@@ -306,19 +422,47 @@ fn encrypt_envelop_shared_secret(key: &[u8],
|
||||
Ok(TinyEncryptEnvelop {
|
||||
r#type: envelop.r#type,
|
||||
kid: envelop.kid.clone(),
|
||||
desc: envelop.desc.clone(),
|
||||
desc: None,
|
||||
encrypted_key: encoded_wrap_key,
|
||||
})
|
||||
}
|
||||
|
||||
fn encrypt_envelop_pgp(key: &[u8], envelop: &TinyEncryptConfigEnvelop) -> XResult<TinyEncryptEnvelop> {
|
||||
let pgp_public_key = opt_result!(parse_spki(&envelop.public_part), "Parse PGP public key failed: {}");
|
||||
fn encrypt_envelop_rsa(key: &[u8], envelop: &TinyEncryptConfigEnvelop) -> XResult<TinyEncryptEnvelop> {
|
||||
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 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 {
|
||||
r#type: envelop.r#type,
|
||||
kid: envelop.kid.clone(),
|
||||
desc: envelop.desc.clone(),
|
||||
desc: None,
|
||||
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
165
src/cmd_execenv.rs
Normal 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
|
||||
}
|
||||
101
src/cmd_info.rs
101
src/cmd_info.rs
@@ -4,46 +4,61 @@ use std::path::PathBuf;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use clap::Args;
|
||||
use rust_util::{util_time, iff, opt_result, simple_error, success, 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 crate::consts::{DATE_TIME_FORMAT, TINY_ENC_AES_GCM, TINY_ENC_FILE_EXT};
|
||||
use crate::file;
|
||||
use crate::{config, util, util_enc_file, util_envelop};
|
||||
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)]
|
||||
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
|
||||
pub paths: Vec<PathBuf>,
|
||||
/// Show raw meta
|
||||
#[arg(long, default_value_t = false)]
|
||||
pub raw_meta: bool,
|
||||
}
|
||||
|
||||
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() {
|
||||
let path = config::resolve_path_namespace(&config, path, true);
|
||||
if i > 0 { println!("{}", "-".repeat(88)); }
|
||||
if let Err(e) = info_single(path, &cmd_info) {
|
||||
warning!("Parse Tiny Encrypt file info failed: {}", e);
|
||||
if let Err(e) = info_single(&path, &cmd_info, &config) {
|
||||
failure!("Parse Tiny Encrypt file info failed: {}", e);
|
||||
}
|
||||
}
|
||||
println!();
|
||||
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());
|
||||
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);
|
||||
}
|
||||
|
||||
let mut file_in = opt_result!(File::open(path), "Open file: {} failed: {}", &path_display);
|
||||
let meta = opt_result!(
|
||||
file::read_tiny_encrypt_meta_and_normalize(&mut file_in), "Read file: {}, failed: {}", &path_display
|
||||
let file_in_len = file_in.metadata().map(|m| m.len()).unwrap_or(0);
|
||||
|
||||
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 {
|
||||
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(());
|
||||
}
|
||||
|
||||
@@ -51,34 +66,61 @@ pub fn info_single(path: &PathBuf, cmd_info: &CmdInfo) -> XResult<()> {
|
||||
infos.push("Tiny Encrypt File Info".to_string());
|
||||
let compressed = if meta.compress { " [compressed]" } else { "" };
|
||||
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: {}",
|
||||
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 fmt = simpledateformat::fmt(DATE_TIME_FORMAT).unwrap();
|
||||
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))
|
||||
));
|
||||
infos.push(format!("{}: {}, {} ago",
|
||||
header("Created"),
|
||||
fmt.format_local(SystemTime::from_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
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(envelops) = meta.envelops.as_ref() {
|
||||
envelops.iter().enumerate().for_each(|(i, envelop)| {
|
||||
let kid = iff!(envelop.kid.is_empty(), "".into(), format!(", Kid: {}", envelop.kid));
|
||||
let desc = envelop.desc.as_ref().map(|desc| format!(", Desc: {}", desc)).unwrap_or_else(|| "".to_string());
|
||||
infos.push(format!("{}: {}{}{}",
|
||||
infos.push(format!("{}: {}",
|
||||
header(&format!("Envelop #{}", i + 1)),
|
||||
envelop.r#type.get_upper_name(),
|
||||
kid,
|
||||
desc
|
||||
util_envelop::format_envelop(envelop, config)
|
||||
));
|
||||
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"));
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
@@ -90,11 +132,8 @@ pub fn info_single(path: &PathBuf, cmd_info: &CmdInfo) -> XResult<()> {
|
||||
}
|
||||
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)));
|
||||
let encryption_algorithm = if let Some(encryption_algorithm) = &meta.encryption_algorithm {
|
||||
encryption_algorithm.to_string()
|
||||
} else {
|
||||
format!("{} (default)", TINY_ENC_AES_GCM)
|
||||
};
|
||||
let encryption_algorithm = meta.encryption_algorithm.clone()
|
||||
.unwrap_or_else(|| format!("{} (default)", TINY_ENC_AES_GCM));
|
||||
infos.push(format!("{}: {}", header("Encryption algorithm"), encryption_algorithm));
|
||||
|
||||
success!("{}", infos.join("\n"));
|
||||
@@ -107,5 +146,5 @@ fn header(h: &str) -> 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
185
src/cmd_initkeychain.rs
Normal 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
145
src/cmd_initpiv.rs
Normal 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)
|
||||
}
|
||||
334
src/cmd_simple_encrypt_decrypt.rs
Normal file
334
src/cmd_simple_encrypt_decrypt.rs
Normal 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);
|
||||
}
|
||||
@@ -1,14 +1,26 @@
|
||||
use clap::Args;
|
||||
use rust_util::XResult;
|
||||
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!(
|
||||
"{} - {}\n{}\n",
|
||||
env!("CARGO_PKG_NAME"),
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
"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(())
|
||||
|
||||
@@ -2,7 +2,13 @@ use std::io::Write;
|
||||
|
||||
use flate2::Compression;
|
||||
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);
|
||||
@@ -28,13 +34,13 @@ impl GzStreamEncoder {
|
||||
}
|
||||
|
||||
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);
|
||||
Self { gz_encoder }
|
||||
}
|
||||
|
||||
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 result = inner.clone();
|
||||
inner.clear();
|
||||
@@ -42,10 +48,7 @@ impl GzStreamEncoder {
|
||||
}
|
||||
|
||||
pub fn finalize(self) -> XResult<Vec<u8>> {
|
||||
match self.gz_encoder.finish() {
|
||||
Ok(last_buffer) => Ok(last_buffer),
|
||||
Err(e) => simple_error!("Decode stream failed: {}", e),
|
||||
}
|
||||
Ok(opt_result!(self.gz_encoder.finish(), "Encode Gz stream failed: {}"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,13 +58,13 @@ pub struct GzStreamDecoder {
|
||||
|
||||
impl GzStreamDecoder {
|
||||
pub fn new() -> Self {
|
||||
let buffer = Vec::with_capacity(1024 * 8);
|
||||
let buffer = Vec::with_capacity(BUFFER_SIZE);
|
||||
let gz_decoder = GzDecoder::new(buffer);
|
||||
Self { gz_decoder }
|
||||
}
|
||||
|
||||
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 result = inner.clone();
|
||||
inner.clear();
|
||||
@@ -69,10 +72,7 @@ impl GzStreamDecoder {
|
||||
}
|
||||
|
||||
pub fn finalize(self) -> XResult<Vec<u8>> {
|
||||
match self.gz_decoder.finish() {
|
||||
Ok(last_buffer) => Ok(last_buffer),
|
||||
Err(e) => simple_error!("Decode stream failed: {}", e),
|
||||
}
|
||||
Ok(opt_result!(self.gz_decoder.finish(), "Decode Gz stream failed: {}"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,7 +96,8 @@ fn test_gzip_compress() {
|
||||
|
||||
#[test]
|
||||
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 mut decoder = GzStreamDecoder::new();
|
||||
let mut decompressed_bytes = vec![];
|
||||
|
||||
434
src/config.rs
434
src/config.rs
@@ -1,12 +1,15 @@
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
|
||||
use rust_util::{debugging, opt_result, simple_error, XResult};
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::{env, fs};
|
||||
use rust_util::{util_env as rust_util_env};
|
||||
use rust_util::util_file::resolve_file_path;
|
||||
use rust_util::{debugging, opt_result, warning, XResult};
|
||||
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::util::decode_base64;
|
||||
|
||||
/// Config file sample:
|
||||
/// ~/.tinyencrypt/config-rs.json
|
||||
@@ -20,6 +23,7 @@ use crate::spec::TinyEncryptEnvelopType;
|
||||
/// },
|
||||
/// {
|
||||
/// "type": "ecdh",
|
||||
/// "sid": "SHORT-ID-1",
|
||||
/// "kid": "KID-2",
|
||||
/// "desc": "this is key 002",
|
||||
/// "publicPart": "04..."
|
||||
@@ -33,45 +37,141 @@ use crate::spec::TinyEncryptEnvelopType;
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
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 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)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TinyEncryptConfigEnvelop {
|
||||
pub r#type: TinyEncryptEnvelopType,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub sid: Option<String>,
|
||||
pub kid: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub desc: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub args: Option<Vec<String>>,
|
||||
pub public_part: String,
|
||||
pub profiles: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
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 config_contents = opt_result!(
|
||||
fs::read_to_string(&resolved_file), "Read file: {}, failed: {}", file
|
||||
let config_content = opt_result!(
|
||||
fs::read_to_string(resolved_file),
|
||||
"Read config file: {}, failed: {}",
|
||||
file
|
||||
);
|
||||
let mut config: TinyEncryptConfig = opt_result!(
|
||||
serde_json::from_str(&config_contents),"Parse file: {}, failed: {}", file);
|
||||
let mut splitted_profiles = HashMap::new();
|
||||
for (k, v) in config.profiles.into_iter() {
|
||||
if !k.contains(',') {
|
||||
splitted_profiles.insert(k, v);
|
||||
} else {
|
||||
k.split(',')
|
||||
.map(|k| k.trim())
|
||||
.filter(|k| !k.is_empty())
|
||||
.for_each(|k| {
|
||||
splitted_profiles.insert(k.to_string(), v.clone());
|
||||
});
|
||||
Self::load_content(&config_content, file)
|
||||
}
|
||||
|
||||
pub fn load_content(config_content: &str, file: &str) -> XResult<Self> {
|
||||
let config: TinyEncryptConfig = opt_result!(
|
||||
serde_json::from_str(&config_content),
|
||||
"Parse config file: {}, failed: {}",
|
||||
file
|
||||
);
|
||||
debugging!("Config: {:#?}", config);
|
||||
let config = load_includes_and_merge(config);
|
||||
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)
|
||||
}
|
||||
|
||||
pub fn resolve_path_namespace(&self, path: &Path, append_te: bool) -> PathBuf {
|
||||
if let Some(path_str) = path.to_str() {
|
||||
if path_str.starts_with(':') {
|
||||
let namespace = path_str
|
||||
.chars()
|
||||
.skip(1)
|
||||
.take_while(|c| *c != ':')
|
||||
.collect::<String>();
|
||||
let mut filename = path_str
|
||||
.chars()
|
||||
.skip(1)
|
||||
.skip_while(|c| *c != ':')
|
||||
.skip(1)
|
||||
.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),
|
||||
}
|
||||
}
|
||||
}
|
||||
path.to_path_buf()
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
@@ -81,40 +181,286 @@ impl TinyEncryptConfig {
|
||||
}
|
||||
|
||||
pub fn find_by_kid(&self, kid: &str) -> Option<&TinyEncryptConfigEnvelop> {
|
||||
self.envelops.iter().find(|e| e.kid == kid)
|
||||
self.find_by_kid_or_filter(kid, |_| false).first().copied()
|
||||
}
|
||||
|
||||
pub fn find_envelops(&self, profile: &Option<String>) -> XResult<Vec<&TinyEncryptConfigEnvelop>> {
|
||||
let profile = profile.as_ref().map(String::as_str).unwrap_or("default");
|
||||
debugging!("Profile: {}", profile);
|
||||
let mut matched_envelops_map = HashMap::new();
|
||||
if let Some(key_ids) = self.profiles.get(profile) {
|
||||
if key_ids.is_empty() {
|
||||
return simple_error!("Profile: {} contains no valid envelopes", profile);
|
||||
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;
|
||||
}
|
||||
for key_id in key_ids {
|
||||
self.envelops.iter().for_each(|envelop| {
|
||||
let is_matched = (&envelop.kid == key_id)
|
||||
|| key_id == &format!("type:{}", &envelop.r#type.get_name());
|
||||
if is_matched {
|
||||
matched_envelops_map.insert(&envelop.kid, envelop);
|
||||
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()));
|
||||
}
|
||||
}
|
||||
let mut envelops: Vec<_> = matched_envelops_map.values()
|
||||
.copied()
|
||||
.collect();
|
||||
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() {
|
||||
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| {
|
||||
if e1.r#type < e2.r#type { return Ordering::Greater; }
|
||||
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; }
|
||||
if e1.r#type < e2.r#type {
|
||||
return Ordering::Greater;
|
||||
}
|
||||
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
|
||||
});
|
||||
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
|
||||
}
|
||||
|
||||
@@ -2,12 +2,28 @@
|
||||
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;
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
use aes_gcm_stream::{Aes256GcmStreamDecryptor, Aes256GcmStreamEncryptor};
|
||||
use rust_util::{opt_result, XResult};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
pub fn try_aes_gcm_decrypt_with_salt(key: &[u8], nonce: &[u8], salt: &[u8], message: &[u8]) -> XResult<Vec<u8>> {
|
||||
let new_nonce = build_salted_nonce(nonce, salt);
|
||||
if let Ok(decrypted) = aes_gcm_decrypt(key, &new_nonce, message) {
|
||||
return Ok(decrypted);
|
||||
}
|
||||
aes_gcm_decrypt(key, nonce, message)
|
||||
}
|
||||
|
||||
pub fn aes_gcm_decrypt(key: &[u8], nonce: &[u8], message: &[u8]) -> XResult<Vec<u8>> {
|
||||
let mut 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);
|
||||
key.zeroize();
|
||||
Ok(b1)
|
||||
}
|
||||
|
||||
pub fn aes_gcm_encrypt_with_salt(key: &[u8], nonce: &[u8], salt: &[u8], message: &[u8]) -> XResult<Vec<u8>> {
|
||||
let new_nonce = build_salted_nonce(nonce, salt);
|
||||
aes_gcm_encrypt(key, &new_nonce, message)
|
||||
}
|
||||
|
||||
pub fn aes_gcm_encrypt(key: &[u8], nonce: &[u8], message: &[u8]) -> XResult<Vec<u8>> {
|
||||
let mut 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);
|
||||
key.zeroize();
|
||||
Ok(b1)
|
||||
}
|
||||
|
||||
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 = hex::decode(sha256::digest(nonce_with_salt)).unwrap();
|
||||
input[0..12].to_vec()
|
||||
}
|
||||
|
||||
#[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
190
src/crypto_cryptor.rs
Normal 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
35
src/crypto_simple.rs
Normal 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()
|
||||
}
|
||||
60
src/file.rs
60
src/file.rs
@@ -1,60 +0,0 @@
|
||||
use std::io::{Read, Write};
|
||||
|
||||
use flate2::Compression;
|
||||
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<W: Write>(w: &mut W, meta: &TinyEncryptMeta, compress_meta: bool) -> XResult<usize> {
|
||||
let tag = iff!(compress_meta, TINY_ENC_COMPRESSED_MAGIC_TAG, TINY_ENC_MAGIC_TAG);
|
||||
opt_result!(w.write_all(&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(Compression::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!(w.write_all(&encrypted_meta_bytes_len.to_be_bytes()), "Write meta len failed: {}");
|
||||
opt_result!(w.write_all(&encrypted_meta_bytes), "Write meta failed: {}");
|
||||
|
||||
Ok(encrypted_meta_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);
|
||||
let is_normal_tiny_enc = tag == TINY_ENC_MAGIC_TAG;
|
||||
let is_compressed_tiny_enc = tag == TINY_ENC_COMPRESSED_MAGIC_TAG;
|
||||
if !is_normal_tiny_enc && !is_compressed_tiny_enc {
|
||||
return simple_error!("Tag is not 0x01 or 0x02, 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);
|
||||
}
|
||||
|
||||
debugging!("Encrypted meta len: {}", length);
|
||||
let mut meta_buff = vec![0; length as usize];
|
||||
opt_result!(r.read_exact(meta_buff.as_mut_slice()), "Read meta failed: {}");
|
||||
|
||||
debugging!("Tiny enc meta compressed: {}", is_compressed_tiny_enc);
|
||||
if is_compressed_tiny_enc {
|
||||
meta_buff = opt_result!(compress::decompress(&meta_buff), "Decompress meta failed: {}");
|
||||
}
|
||||
debugging!("Encrypted meta: {}", String::from_utf8_lossy(&meta_buff));
|
||||
|
||||
Ok(opt_result!(serde_json::from_slice(&meta_buff), "Parse meta failed: {}"))
|
||||
}
|
||||
82
src/lib.rs
Normal file
82
src/lib.rs
Normal 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;
|
||||
|
||||
87
src/main.rs
87
src/main.rs
@@ -2,32 +2,21 @@ extern crate core;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use rust_util::XResult;
|
||||
use crate::cmd_config::CmdConfig;
|
||||
|
||||
use crate::cmd_decrypt::CmdDecrypt;
|
||||
use crate::cmd_encrypt::CmdEncrypt;
|
||||
use crate::cmd_info::CmdInfo;
|
||||
use crate::cmd_version::CmdVersion;
|
||||
#[cfg(feature = "decrypt")]
|
||||
use tiny_encrypt::CmdDecrypt;
|
||||
#[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 consts;
|
||||
mod util;
|
||||
mod util_piv;
|
||||
mod util_ecdh;
|
||||
mod util_p384;
|
||||
mod util_x25519;
|
||||
mod compress;
|
||||
mod config;
|
||||
mod spec;
|
||||
mod crypto_aes;
|
||||
mod crypto_rsa;
|
||||
mod wrap_key;
|
||||
mod file;
|
||||
mod card;
|
||||
mod cmd_version;
|
||||
mod cmd_config;
|
||||
mod cmd_info;
|
||||
mod cmd_decrypt;
|
||||
mod cmd_encrypt;
|
||||
use zeroizing_alloc::ZeroAlloc;
|
||||
|
||||
#[global_allocator]
|
||||
static ALLOC: ZeroAlloc<std::alloc::System> = ZeroAlloc(std::alloc::System);
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(name = "tiny-encrypt-rs")]
|
||||
@@ -42,27 +31,63 @@ enum Commands {
|
||||
/// Encrypt file(s)
|
||||
#[command(arg_required_else_help = true, short_flag = 'e')]
|
||||
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)
|
||||
#[command(arg_required_else_help = true, short_flag = 'd')]
|
||||
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')]
|
||||
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 Config
|
||||
/// Show configuration
|
||||
#[command(short_flag = 'c')]
|
||||
Config(CmdConfig),
|
||||
}
|
||||
|
||||
fn main() -> XResult<()> {
|
||||
init_tiny_encrypt_log();
|
||||
|
||||
let args = Cli::parse();
|
||||
match args.command {
|
||||
Commands::Encrypt(cmd_encrypt) => cmd_encrypt::encrypt(cmd_encrypt),
|
||||
Commands::Decrypt(cmd_decrypt) => cmd_decrypt::decrypt(cmd_decrypt),
|
||||
Commands::Info(cmd_info) => cmd_info::info(cmd_info),
|
||||
Commands::Version(cmd_version) => cmd_version::version(cmd_version),
|
||||
Commands::Config(cmd_config) => cmd_config::config(cmd_config),
|
||||
Commands::Encrypt(cmd_encrypt) => tiny_encrypt::encrypt(cmd_encrypt),
|
||||
Commands::SimpleEncrypt(cmd_simple_encrypt) => tiny_encrypt::simple_encrypt(cmd_simple_encrypt),
|
||||
#[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),
|
||||
}
|
||||
}
|
||||
193
src/spec.rs
193
src/spec.rs
@@ -1,15 +1,16 @@
|
||||
use std::fs::Metadata;
|
||||
|
||||
use flate2::Compression;
|
||||
use rust_util::{opt_result, util_time, XResult};
|
||||
use rust_util::util_time::get_millis;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{compress, crypto_aes};
|
||||
use crate::consts::{SALT_META, 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: &str = "1.0";
|
||||
// Compatible with 1.0 is removed from v0.6.0
|
||||
// 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://github.com/OpenWebStandard/tiny-encrypt-format-spec/blob/main/TinyEncryptSpecv1.1.md)
|
||||
@@ -20,6 +21,8 @@ pub struct TinyEncryptMeta {
|
||||
pub created: u64,
|
||||
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>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub encrypted_comment: Option<String>,
|
||||
@@ -47,6 +50,8 @@ pub struct TinyEncryptMeta {
|
||||
pub nonce: String,
|
||||
pub file_length: u64,
|
||||
pub file_last_modified: u64,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub file_edit_count: Option<u64>,
|
||||
pub compress: bool,
|
||||
}
|
||||
|
||||
@@ -63,16 +68,55 @@ pub struct TinyEncryptEnvelop {
|
||||
/// NOTICE: Kms and Age is not being supported in the future
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, PartialOrd)]
|
||||
pub enum TinyEncryptEnvelopType {
|
||||
#[serde(rename = "pgp")]
|
||||
Pgp,
|
||||
// OpenPGP Card RSA
|
||||
#[serde(rename = "pgp-rsa", alias = "pgp")]
|
||||
PgpRsa,
|
||||
// OpenPGP Card X25519
|
||||
#[serde(rename = "pgp-x25519")]
|
||||
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")]
|
||||
Age,
|
||||
#[serde(rename = "ecdh")]
|
||||
Ecdh,
|
||||
#[serde(rename = "ecdh-p384")]
|
||||
EcdhP384,
|
||||
// KMS, tiny-encrypt-rs is not supported
|
||||
#[serde(rename = "kms")]
|
||||
Kms,
|
||||
}
|
||||
@@ -81,16 +125,96 @@ impl TinyEncryptEnvelopType {
|
||||
pub fn get_upper_name(&self) -> String {
|
||||
self.get_name().to_uppercase()
|
||||
}
|
||||
|
||||
pub fn get_name(&self) -> &'static str {
|
||||
match self {
|
||||
TinyEncryptEnvelopType::Pgp => "pgp",
|
||||
TinyEncryptEnvelopType::PgpRsa => "pgp-rsa",
|
||||
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::Ecdh => "ecdh",
|
||||
TinyEncryptEnvelopType::EcdhP384 => "ecdh-p384",
|
||||
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)]
|
||||
@@ -104,21 +228,21 @@ pub struct EncEncryptedMeta {
|
||||
}
|
||||
|
||||
impl EncEncryptedMeta {
|
||||
pub fn unseal(key: &[u8], nonce: &[u8], message: &[u8]) -> XResult<Self> {
|
||||
let mut decrypted = opt_result!(crypto_aes::try_aes_gcm_decrypt_with_salt(
|
||||
key, nonce, SALT_META, message), "Decrypt failed: {}");
|
||||
decrypted = opt_result!(compress::decompress(&decrypted), "Decode faield: {}");
|
||||
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 failed: {}");
|
||||
serde_json::from_slice::<Self>(&decrypted), "Parse encrypted meta failed: {}");
|
||||
Ok(meta)
|
||||
}
|
||||
|
||||
pub fn seal(&self, key: &[u8], nonce: &[u8]) -> XResult<Vec<u8>> {
|
||||
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(Compression::default(), &encrypted_meta_json), "Compress failed: {}");
|
||||
let encrypted = opt_result!(crypto_aes::aes_gcm_encrypt_with_salt(
|
||||
key, nonce, SALT_META, encrypted_meta_json.as_slice()), "Encrypt failed: {}");
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -131,11 +255,12 @@ pub struct EncMetadata {
|
||||
}
|
||||
|
||||
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 {
|
||||
version: TINY_ENCRYPT_VERSION_11.to_string(),
|
||||
created: util_time::get_current_millis() as u64,
|
||||
user_agent: get_user_agent(),
|
||||
latest_user_agent: None,
|
||||
comment: enc_metadata.comment.to_owned(),
|
||||
encrypted_comment: enc_metadata.encrypted_comment.to_owned(),
|
||||
encrypted_meta: enc_metadata.encrypted_meta.to_owned(),
|
||||
@@ -147,28 +272,30 @@ impl TinyEncryptMeta {
|
||||
ecdh_point: None,
|
||||
envelop: None,
|
||||
envelops: Some(envelops),
|
||||
encryption_algorithm: Some(TINY_ENC_AES_GCM.to_string()),
|
||||
encryption_algorithm: Some(cryptor.get_name()),
|
||||
nonce: encode_base64(nonce),
|
||||
file_length: metadata.len(),
|
||||
file_last_modified: match metadata.modified() {
|
||||
Ok(modified) => get_millis(&modified) as u64,
|
||||
Err(_) => 0,
|
||||
},
|
||||
file_edit_count: None,
|
||||
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() {
|
||||
self.envelops = Some(vec![]);
|
||||
}
|
||||
self.normalize_envelop();
|
||||
self.normalize_pgp_envelop();
|
||||
self.normalize_kms_envelop();
|
||||
self.normalize_pgp_rsa_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) {
|
||||
envelops.push(TinyEncryptEnvelop {
|
||||
r#type: TinyEncryptEnvelopType::Kms,
|
||||
@@ -180,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))
|
||||
= (&self.pgp_envelop, &self.pgp_fingerprint, &mut self.envelops) {
|
||||
envelops.push(TinyEncryptEnvelop {
|
||||
r#type: TinyEncryptEnvelopType::Pgp,
|
||||
r#type: TinyEncryptEnvelopType::PgpRsa,
|
||||
kid: pgp_fingerprint.into(),
|
||||
desc: None,
|
||||
encrypted_key: pgp_envelop.into(),
|
||||
@@ -208,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))
|
||||
= (&self.ecdh_envelop, &self.ecdh_point, &mut self.envelops) {
|
||||
envelops.push(TinyEncryptEnvelop {
|
||||
r#type: TinyEncryptEnvelopType::Ecdh,
|
||||
r#type: TinyEncryptEnvelopType::PivP256,
|
||||
kid: ecdh_point.into(),
|
||||
desc: None,
|
||||
encrypted_key: ecdh_envelop.into(),
|
||||
|
||||
69
src/temporary_key.rs
Normal file
69
src/temporary_key.rs
Normal 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())
|
||||
}
|
||||
280
src/util.rs
280
src/util.rs
@@ -1,14 +1,112 @@
|
||||
use std::{fs, io};
|
||||
use std::io::Write;
|
||||
use std::io::{Read, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::{fs, io};
|
||||
|
||||
use base64::Engine;
|
||||
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 rust_util::{information, 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 crate::consts::TINY_ENC_FILE_EXT;
|
||||
use crate::consts::{TINY_ENC_FILE_EXT, TINY_ENC_PEM_FILE_EXT};
|
||||
use crate::util_digest::DigestWrite;
|
||||
use crate::util_env;
|
||||
|
||||
pub struct SecVec(pub Vec<u8>);
|
||||
|
||||
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) {
|
||||
@@ -57,25 +155,62 @@ 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 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);
|
||||
key.zeroize();
|
||||
nonce.zeroize();
|
||||
result
|
||||
(SecVec(key_vec), SecVec(nonce_vec))
|
||||
}
|
||||
|
||||
pub fn simple_kdf(input: &[u8]) -> Vec<u8> {
|
||||
let input = hex::decode(sha256::digest(input)).unwrap();
|
||||
let input = hex::decode(sha256::digest(input)).unwrap();
|
||||
let input = hex::decode(sha256::digest(input)).unwrap();
|
||||
let input = hex::decode(sha256::digest(input)).unwrap();
|
||||
let input = hex::decode(sha256::digest(input)).unwrap();
|
||||
let input = hex::decode(sha256::digest(input)).unwrap();
|
||||
let input = hex::decode(sha256::digest(input)).unwrap();
|
||||
hex::decode(sha256::digest(input)).unwrap()
|
||||
let mut input = input.to_vec();
|
||||
for _ in 0..8 {
|
||||
let mut sha256 = DigestWrite::sha256();
|
||||
sha256.write_all(&input).expect("SHOULD NOT HAPPEN");
|
||||
input = sha256.digest();
|
||||
}
|
||||
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>> {
|
||||
@@ -96,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 {
|
||||
loop {
|
||||
print!("{} ({}-{}): ", hint, from, to);
|
||||
print_ex!("{} ({}-{}): ", hint, from, to);
|
||||
io::stdout().flush().ok();
|
||||
let mut buff = String::new();
|
||||
let _ = io::stdin().read_line(&mut buff).expect("Read line from stdin");
|
||||
@@ -112,30 +247,62 @@ 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 {
|
||||
format!("TinyEncrypt-rs v{}@{}", env!("CARGO_PKG_VERSION"),
|
||||
if cfg!(target_os = "macos") {
|
||||
"MacOS"
|
||||
} else if cfg!(target_os = "ios") {
|
||||
"iOS"
|
||||
} else if cfg!(target_os = "android") {
|
||||
"Android"
|
||||
} else if cfg!(target_os = "windows") {
|
||||
"Windows"
|
||||
} else if cfg!(target_os = "linux") {
|
||||
"Linux"
|
||||
} else if cfg!(target_os = "freebsd") {
|
||||
"FreeBSD"
|
||||
} else if cfg!(target_os = "dragonfly") {
|
||||
"Dragonfly"
|
||||
} else if cfg!(target_os = "openbsd") {
|
||||
"OpenBSD"
|
||||
} else if cfg!(target_os = "netbsd") {
|
||||
"NetBSD"
|
||||
} else {
|
||||
panic!("Unsupported OS!");
|
||||
}
|
||||
)
|
||||
format!("TinyEncrypt-rs v{}@{}-{}", env!("CARGO_PKG_VERSION"), get_os(), get_arch())
|
||||
}
|
||||
|
||||
pub fn get_os() -> String {
|
||||
if cfg!(target_os = "macos") {
|
||||
"macOS"
|
||||
} else if cfg!(target_os = "ios") {
|
||||
"iOS"
|
||||
} else if cfg!(target_os = "android") {
|
||||
"Android"
|
||||
} else if cfg!(target_os = "windows") {
|
||||
"Windows"
|
||||
} else if cfg!(target_os = "linux") {
|
||||
"Linux"
|
||||
} else if cfg!(target_os = "freebsd") {
|
||||
"FreeBSD"
|
||||
} else if cfg!(target_os = "dragonfly") {
|
||||
"Dragonfly"
|
||||
} else if cfg!(target_os = "openbsd") {
|
||||
"OpenBSD"
|
||||
} 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) {
|
||||
@@ -144,9 +311,40 @@ pub fn zeroize(object: impl Zeroize) {
|
||||
}
|
||||
|
||||
pub fn read_line(ln: &str) {
|
||||
print!("{}", ln);
|
||||
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
83
src/util_digest.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
107
src/util_ecdh.rs
107
src/util_ecdh.rs
@@ -1,35 +1,80 @@
|
||||
use p256::ecdh::EphemeralSecret;
|
||||
use rand::rngs::OsRng;
|
||||
use rust_util::{opt_result, XResult};
|
||||
pub mod ecdh_p256 {
|
||||
use std::ops::Deref;
|
||||
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;
|
||||
use p256::{EncodedPoint, PublicKey};
|
||||
use p256::elliptic_curve::sec1::FromEncodedPoint;
|
||||
// use p256::elliptic_curve::sec1::{FromEncodedPoint, ToEncodedPoint};
|
||||
pub fn compute_p256_shared_secret(public_key_point_hex: &str) -> XResult<(Vec<u8>, Vec<u8>)> {
|
||||
let public_key_point_bytes = opt_result!(hex::decode(public_key_point_hex), "Parse public key point hex failed: {}");
|
||||
let encoded_point = opt_result!(EncodedPoint::from_bytes(public_key_point_bytes), "Parse public key point failed: {}");
|
||||
let public_key = PublicKey::from_encoded_point(&encoded_point).unwrap();
|
||||
|
||||
// #[derive(Debug)]
|
||||
// pub struct EphemeralKeyBytes(EncodedPoint);
|
||||
//
|
||||
// impl EphemeralKeyBytes {
|
||||
// pub fn from_public_key(epk: &PublicKey) -> Self {
|
||||
// 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)
|
||||
// }
|
||||
// }
|
||||
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 fn compute_shared_secret(public_key_point_hex: &str) -> XResult<(Vec<u8>, Vec<u8>)> {
|
||||
let public_key_point_bytes = opt_result!(hex::decode(public_key_point_hex), "Parse public key point hex failed: {}");
|
||||
let encoded_point = opt_result!(EncodedPoint::from_bytes(public_key_point_bytes), "Parse public key point failed: {}");
|
||||
let public_key = PublicKey::from_encoded_point(&encoded_point).unwrap();
|
||||
pub mod ecdh_p384 {
|
||||
use std::ops::Deref;
|
||||
use p384::{EncodedPoint, PublicKey};
|
||||
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);
|
||||
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().as_slice().to_vec(), epk_public_key_der.to_vec()))
|
||||
}
|
||||
pub fn compute_p384_shared_secret(public_key_point_hex: &str) -> XResult<(Vec<u8>, Vec<u8>)> {
|
||||
let public_key_point_bytes = opt_result!(hex::decode(public_key_point_hex), "Parse public key point hex failed: {}");
|
||||
let encoded_point = opt_result!(EncodedPoint::from_bytes(public_key_point_bytes), "Parse public key point failed: {}");
|
||||
let public_key = PublicKey::from_encoded_point(&encoded_point).unwrap();
|
||||
|
||||
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
61
src/util_enc_file.rs
Normal 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
91
src/util_env.rs
Normal 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
34
src/util_envelop.rs
Normal 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
25
src/util_file.rs
Normal 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
107
src/util_gpg.rs
Normal 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
47
src/util_keychainkey.rs
Normal 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
294
src/util_keychainstatic.rs
Normal 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
25
src/util_log.rs
Normal 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
49
src/util_mlkem.rs
Normal 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.")
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
use p384::ecdh::EphemeralSecret;
|
||||
use rand::rngs::OsRng;
|
||||
use rust_util::{opt_result, XResult};
|
||||
|
||||
use p384::pkcs8::EncodePublicKey;
|
||||
use p384::{EncodedPoint, PublicKey};
|
||||
use p384::elliptic_curve::sec1::FromEncodedPoint;
|
||||
|
||||
pub fn compute_p384_shared_secret(public_key_point_hex: &str) -> XResult<(Vec<u8>, Vec<u8>)> {
|
||||
let public_key_point_bytes = opt_result!(hex::decode(public_key_point_hex), "Parse public key point hex failed: {}");
|
||||
let encoded_point = opt_result!(EncodedPoint::from_bytes(public_key_point_bytes), "Parse public key point failed: {}");
|
||||
let public_key = PublicKey::from_encoded_point(&encoded_point).unwrap();
|
||||
|
||||
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().as_slice().to_vec(), epk_public_key_der.to_vec()))
|
||||
}
|
||||
39
src/util_pgp.rs
Normal file
39
src/util_pgp.rs
Normal 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"))
|
||||
}
|
||||
@@ -1,6 +1,39 @@
|
||||
use rust_util::{simple_error, XResult};
|
||||
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() {
|
||||
|
||||
46
src/util_progress.rs
Normal file
46
src/util_progress.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,40 +4,21 @@ use x509_parser::prelude::FromDer;
|
||||
use x509_parser::public_key::RSAPublicKey;
|
||||
use x509_parser::x509::SubjectPublicKeyInfo;
|
||||
|
||||
use crate::util::decode_base64;
|
||||
use crate::util;
|
||||
|
||||
/// Parse RSA Subject Public Key Info(SPKI) to Rsa Public Key
|
||||
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 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 rsa_public_key = opt_result!(RsaPublicKey::new(
|
||||
BigUint::from_bytes_be(public_key.modulus),
|
||||
BigUint::from_bytes_be(public_key.exponent),
|
||||
), "Parse Rsa public key failed: {}");
|
||||
), "Parse RSA public key failed: {}");
|
||||
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]
|
||||
fn test_parse_spki() {
|
||||
use rsa::traits::PublicKeyParts;
|
||||
@@ -110,7 +91,7 @@ qaCoQsuRtnowGKzrbVdinukd1wj0LkBuz2oNMB3qsXyq7QtOxiFTuKkMOoQNUiCE
|
||||
KQIDAQAB
|
||||
-----END PUBLIC KEY-----";
|
||||
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 = RsaPrivateKey::try_from(private_key_info).unwrap();
|
||||
let mut rng = rand::thread_rng();
|
||||
175
src/util_simple_pbe.rs
Normal file
175
src/util_simple_pbe.rs
Normal 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());
|
||||
}
|
||||
@@ -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()))
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
use rust_util::{opt_result, simple_error, XResult};
|
||||
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)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -20,22 +22,38 @@ pub struct WrapKeyHeader {
|
||||
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 {
|
||||
pub fn encode(&self) -> XResult<String> {
|
||||
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_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('.');
|
||||
buf.push_str(&encode_base64_url_no_pad(&self.nonce));
|
||||
buf.push_str(&nonce_str);
|
||||
buf.push('.');
|
||||
buf.push_str(&encode_base64_url_no_pad(&self.encrypted_data));
|
||||
buf.push_str(&encrypted_data_str);
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
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:");
|
||||
}
|
||||
let wks = wk.split('.').collect::<Vec<_>>();
|
||||
@@ -43,13 +61,13 @@ impl WrapKey {
|
||||
return simple_error!("Invalid wrap key.");
|
||||
}
|
||||
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 encrypted_data = wks[2];
|
||||
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 nonce = opt_result!(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 nonce = opt_result!(util::decode_base64_url_no_pad(nonce), "Invalid wrap key: {}");
|
||||
let encrypted_data = opt_result!(util::decode_base64_url_no_pad(encrypted_data), "Invalid wrap key: {}");
|
||||
Ok(WrapKey {
|
||||
header,
|
||||
nonce,
|
||||
|
||||
Reference in New Issue
Block a user