Skip to content

Commit 8831a43

Browse files
committed
Merge branch 'RM-3422_improve_pw_decryt_cli' into 'master'
Rm-3422: improve pw decryt cli See merge request cdoc2/cdoc2-java-ref-impl!22
2 parents 3c8715a + f034d4d commit 8831a43

File tree

10 files changed

+218
-26
lines changed

10 files changed

+218
-26
lines changed

cdoc2-cli/README.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ Default expiration duration will be used if it is not requested by the client. D
6666
expiration durations are configurable values in put-server and get-server.
6767

6868

69-
### Encryption with symmetric key
69+
### Encryption with symmetric key and password
7070

7171
Generate key with openssl (minimum length 32 bytes):
7272
```
@@ -92,7 +92,7 @@ cat keys/b64secret.option --secret "label_b64secret:base64,aejUgxxSQXqiiyrxSGACf
9292

9393
Or encrypt with password clear text (note, that password also can be encoded to base64 format, as secret):
9494
```
95-
java -jar target/cdoc2-cli-*.jar create --password "passwordlabel:myPlainTextPassword" -f /tmp/symmetric.cdoc README.md
95+
java -jar target/cdoc2-cli-*.jar create --password "passwordlabel:myPlainTextPassword" -f /tmp/password.cdoc README.md
9696
```
9797

9898
Decryption is done with the same label and key used for encryption
@@ -102,9 +102,15 @@ java -jar target/cdoc2-cli-*.jar decrypt @keys/b64secret.option -f /tmp/symmetri
102102

103103
Or with the same label and password used for encryption:
104104
```
105-
java -jar target/cdoc2-cli-*.jar decrypt --password "passwordlabel:myPlainTextPassword" -f /tmp/symmetric.cdoc --output /tmp
105+
java -jar target/cdoc2-cli-*.jar decrypt --password "passwordlabel:myPlainTextPassword" -f /tmp/password.cdoc --output /tmp
106106
```
107107

108+
If cdoc2 file contains only one password, then specifying label is not required and label can be omitted:
109+
```
110+
java -jar target/cdoc2-cli-*.jar decrypt --password ":myPlainTextPassword" -f /tmp/password.cdoc --output /tmp
111+
```
112+
113+
108114
Or with the same label and secret used for encryption:
109115
```
110116
java -jar target/cdoc2-cli-*.jar decrypt --secret "label_b64secret:base64,aejUgxxSQXqiiyrxSGACfMiIRBZq5KjlCwr/xVNY/B0=" -f /tmp/symmetric.cdoc --output /tmp

cdoc2-cli/src/main/java/ee/cyber/cdoc2/cli/CDocDecryptionHelper.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public static DecryptionKeyMaterial getDecryptionKeyMaterial(
3434
String keyAlias
3535
) throws GeneralSecurityException, IOException, CDocValidationException, CDocParseException {
3636
DecryptionKeyMaterial decryptionKm =
37-
SymmetricKeyUtil.extractDecryptionKeyMaterialFromSymmetricKey(
37+
SymmetricKeyUtil.extractDecryptionKeyMaterial(
3838
cdocFile.toPath(), password, secret
3939
);
4040

cdoc2-cli/src/main/java/ee/cyber/cdoc2/cli/InteractiveCommunicationUtil.java

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,28 +31,32 @@ private InteractiveCommunicationUtil() { }
3131

3232
/**
3333
* Ask password and label interactively.
34+
* @param verifyPw if true then password is asked twice and they must match
3435
* @return FormattedOptionParts with password chars and label
3536
* @throws CDocUserException if password wasn't entered
3637
* @throws IllegalArgumentException if entered passwords don't match
3738
*/
38-
public static FormattedOptionParts readPasswordAndLabelInteractively() {
39+
public static FormattedOptionParts readPasswordAndLabelInteractively(boolean verifyPw) {
40+
3941
Console console = System.console();
4042
char[] password = readPasswordInteractively(console, PROMPT_PASSWORD);
4143
PasswordValidationUtil.validatePassword(password);
4244

43-
char[] reenteredPassword = readPasswordInteractively(console, PROMPT_PASSWORD_REENTER);
45+
if (verifyPw) {
46+
char[] reenteredPassword = readPasswordInteractively(console, PROMPT_PASSWORD_REENTER);
4447

45-
if (!Arrays.equals(password, reenteredPassword)) {
46-
log.info("Passwords don't match");
47-
throw new IllegalArgumentException("Passwords don't match");
48+
if (!Arrays.equals(password, reenteredPassword)) {
49+
log.info("Passwords don't match");
50+
throw new IllegalArgumentException("Passwords don't match");
51+
}
4852
}
4953

5054
String label = readLabelInteractively(console);
5155

5256
return new FormattedOptionParts(password, label, EncryptionKeyOrigin.PASSWORD);
5357
}
5458

55-
private static char[] readPasswordInteractively(Console console, String prompt) throws CDocUserException {
59+
public static char[] readPasswordInteractively(Console console, String prompt) throws CDocUserException {
5660
if (console != null) {
5761
return console.readPassword(prompt);
5862
} else { //running from IDE, console is null

cdoc2-cli/src/main/java/ee/cyber/cdoc2/cli/SymmetricKeyUtil.java

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import java.util.LinkedList;
1010
import java.util.List;
1111

12+
import ee.cyber.cdoc2.container.recipients.PBKDF2Recipient;
1213
import org.slf4j.Logger;
1314
import org.slf4j.LoggerFactory;
1415

@@ -23,6 +24,8 @@
2324
import ee.cyber.cdoc2.crypto.SymmetricKeyTools;
2425
import ee.cyber.cdoc2.util.PasswordValidationUtil;
2526

27+
import javax.annotation.Nullable;
28+
2629

2730
/**
2831
* Symmetric key usage in CDOC is supported by CDOC format, but its use cases are not finalized.
@@ -38,13 +41,15 @@ public final class SymmetricKeyUtil {
3841
private static final String SYMMETRIC_KEY_DESCRIPTION = "symmetric key with label. "
3942
+ "Must have format";
4043

44+
private static final String PW_DESCRIPTION = "password with label. Must have format";
45+
4146
// --secret format description, used in cdoc <cmd> classes
4247
public static final String SECRET_DESCRIPTION = SYMMETRIC_KEY_DESCRIPTION
4348
+ " <label>:<secret>. <secret> is a base64 encoded binary. "
4449
+ "It must be prefixed with `base64,`";
4550

4651
// --password format description, used in cdoc <cmd> classes
47-
public static final String PASSWORD_DESCRIPTION = SYMMETRIC_KEY_DESCRIPTION
52+
public static final String PASSWORD_DESCRIPTION = PW_DESCRIPTION
4853
+ " <label>:<password>. <password> can be plain text or base64 "
4954
+ "encoded binary. In case of base64, it must be prefixed with `base64,`";
5055

@@ -140,23 +145,64 @@ public static FormattedOptionParts splitFormattedOption(
140145
* aejUgxxSQXqiiyrxSGACfMiIRBZq5KjlCwr/xVNY/B0="
141146
* @param formattedPassword formatted as label:password where 2nd param can be base64 encoded
142147
* bytes or regular utf-8 string. Base64 encoded string must be
143-
* prefixed with 'base64,', followed by base64 string
148+
* prefixed with 'base64,', followed by base64 string. If "",
149+
* then password and label are asked interactively.
150+
* @param verifyPw if password should be asked twice to verify that they match (encryption).
144151
* @return FormattedOptionParts with extracted password and label
145152
*/
146-
public static FormattedOptionParts getSplitPasswordAndLabel(String formattedPassword)
153+
public static FormattedOptionParts getSplitPasswordAndLabel(String formattedPassword,
154+
boolean verifyPw)
147155
throws CDocValidationException {
148156
FormattedOptionParts passwordAndLabel;
149157
if (formattedPassword.isEmpty()) {
150-
passwordAndLabel = InteractiveCommunicationUtil.readPasswordAndLabelInteractively();
158+
passwordAndLabel = InteractiveCommunicationUtil.readPasswordAndLabelInteractively(verifyPw);
151159
} else {
152160
passwordAndLabel
153161
= splitFormattedOption(formattedPassword, EncryptionKeyOrigin.PASSWORD);
154-
PasswordValidationUtil.validatePassword(passwordAndLabel.optionChars());
162+
163+
if (verifyPw) {
164+
PasswordValidationUtil.validatePassword(passwordAndLabel.optionChars());
165+
}
155166
}
156167

157168
return passwordAndLabel;
158169
}
159170

171+
/**
172+
* Get password for decryption. Ignore user-entered label when there is only one pbkdf (password) recipient
173+
* in CDOC2 header.
174+
* @param formattedPassword formatted as label:password where
175+
* 2nd param can be base64 encoded bytes or regular utf-8 string.
176+
* Base64 encoded string must be
177+
* prefixed with 'base64,', followed by base64 string. If "",
178+
* then password and label are asked interactively.
179+
* @param pbkdf2Recipients if pbkdf2Recipients.size() == 1 then label is read from CDOC header
180+
* @return
181+
* @throws CDocValidationException
182+
*/
183+
public static FormattedOptionParts getSplitPasswordAndLabel(String formattedPassword,
184+
List<PBKDF2Recipient> pbkdf2Recipients)
185+
throws CDocValidationException {
186+
187+
if (pbkdf2Recipients.size() == 1) {
188+
String label = pbkdf2Recipients.get(0).getRecipientKeyLabel();
189+
if (formattedPassword.isEmpty()) {
190+
char[] pw = InteractiveCommunicationUtil.readPasswordInteractively(System.console(),
191+
InteractiveCommunicationUtil.PROMPT_PASSWORD);
192+
193+
return new FormattedOptionParts(pw, label, EncryptionKeyOrigin.PASSWORD);
194+
} else {
195+
FormattedOptionParts parsed =
196+
splitFormattedOption(formattedPassword, EncryptionKeyOrigin.PASSWORD);
197+
//overwrite label with recipient label to allow entering password without label for
198+
//single pbkdfRecipient eg -pw ":pw"
199+
return new FormattedOptionParts(parsed.optionChars(), label, EncryptionKeyOrigin.PASSWORD);
200+
}
201+
} else {
202+
return getSplitPasswordAndLabel(formattedPassword, false);
203+
}
204+
}
205+
160206
/**
161207
* Extract decryption key material from formatted secret or password "label:topsecret123!"
162208
* or "label123:base64,aejUgxxSQXqiiyrxSGACfMiIRBZq5KjlCwr/xVNY/B0="
@@ -174,20 +220,25 @@ public static FormattedOptionParts getSplitPasswordAndLabel(String formattedPass
174220
* @throws IOException if header parsing has failed
175221
* @throws CDocParseException if recipients deserializing has failed
176222
*/
177-
public static DecryptionKeyMaterial extractDecryptionKeyMaterialFromSymmetricKey(
223+
public static DecryptionKeyMaterial extractDecryptionKeyMaterial(
178224
Path cDocFilePath,
179-
String formattedPassword,
180-
String formattedSecret
225+
@Nullable String formattedPassword,
226+
@Nullable String formattedSecret
181227
) throws CDocValidationException,
182228
GeneralSecurityException,
183229
IOException,
184230
CDocParseException {
185231

186232
List<Recipient> recipients = Envelope.parseHeader(Files.newInputStream(cDocFilePath));
187233

234+
188235
DecryptionKeyMaterial decryptionKm = null;
189236
if (null != formattedPassword) {
190-
FormattedOptionParts splitPassword = getSplitPasswordAndLabel(formattedPassword);
237+
List<PBKDF2Recipient> pbkdf2Recipients = recipients.stream()
238+
.filter(recipient -> recipient instanceof PBKDF2Recipient)
239+
.map(r -> (PBKDF2Recipient)r).toList();
240+
241+
FormattedOptionParts splitPassword = getSplitPasswordAndLabel(formattedPassword, pbkdf2Recipients);
191242
decryptionKm =
192243
SymmetricKeyTools.extractDecryptionKeyMaterialFromPassword(splitPassword, recipients);
193244
}

cdoc2-cli/src/main/java/ee/cyber/cdoc2/cli/commands/CDocCreateCmd.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ private void addSymmetricKeysWithLabels(List<EncryptionKeyMaterial> recipients)
168168
);
169169
if (null != recipient.password) {
170170
FormattedOptionParts password
171-
= SymmetricKeyUtil.getSplitPasswordAndLabel(recipient.password);
171+
= SymmetricKeyUtil.getSplitPasswordAndLabel(recipient.password, true);
172172
recipients.add(SymmetricKeyUtil.extractEncryptionKeyMaterialFromPassword(password));
173173
}
174174
}

cdoc2-cli/src/main/java/ee/cyber/cdoc2/cli/commands/CDocInfoCmd.java

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,29 @@
11
package ee.cyber.cdoc2.cli.commands;
22

33
import ee.cyber.cdoc2.container.Envelope;
4+
import ee.cyber.cdoc2.container.recipients.PBKDF2Recipient;
45
import ee.cyber.cdoc2.container.recipients.PublicKeyRecipient;
56
import ee.cyber.cdoc2.container.recipients.Recipient;
67
import ee.cyber.cdoc2.container.recipients.ServerRecipient;
8+
import ee.cyber.cdoc2.container.recipients.SymmetricKeyRecipient;
9+
import org.slf4j.Logger;
10+
import org.slf4j.LoggerFactory;
711
import picocli.CommandLine;
812

913
import java.io.File;
1014
import java.nio.file.Files;
1115
import java.util.List;
1216
import java.util.Map;
17+
import java.util.Objects;
1318
import java.util.concurrent.Callable;
1419

1520
//S106 Standard outputs should not be used directly to log anything
1621
//CLI needs to interact with standard outputs
1722
@SuppressWarnings("java:S106")
1823
@CommandLine.Command(name = "info", showAtFileInUsageHelp = true)
1924
public class CDocInfoCmd implements Callable<Void> {
25+
26+
private static final Logger log = LoggerFactory.getLogger(CDocInfoCmd.class);
2027
@CommandLine.Option(names = {"-f", "--file" }, required = true,
2128
paramLabel = "CDOC", description = "the CDOC2 file")
2229
private File cdocFile;
@@ -37,9 +44,7 @@ public Void call() throws Exception {
3744
List<Recipient> recipients = Envelope.parseHeader(Files.newInputStream(cdocFile.toPath()));
3845
for (Recipient recipient: recipients) {
3946

40-
String type = (recipient instanceof PublicKeyRecipient)
41-
? ((PublicKeyRecipient) recipient).getRecipientPubKey().getAlgorithm() + " PublicKey"
42-
: "SymmetricKey";
47+
String type = getHumanReadableType(recipient);
4348

4449
String label = recipient.getRecipientKeyLabel();
4550

@@ -52,4 +57,20 @@ public Void call() throws Exception {
5257

5358
return null;
5459
}
60+
61+
String getHumanReadableType(Recipient recipient) {
62+
Objects.requireNonNull(recipient); //can't have null recipient, fail with exception
63+
64+
if (recipient instanceof PublicKeyRecipient) {
65+
return ((PublicKeyRecipient) recipient).getRecipientPubKey().getAlgorithm() + " PublicKey";
66+
} else if (recipient instanceof SymmetricKeyRecipient) {
67+
return "SymmetricKey";
68+
} else if (recipient instanceof PBKDF2Recipient) {
69+
return "Password";
70+
} else {
71+
//unknown recipient type, don't fail as other recipients might be supported
72+
log.warn("Unknown recipient {}", recipient.getClass());
73+
return recipient.getClass().toString();
74+
}
75+
}
5576
}

cdoc2-cli/src/main/java/ee/cyber/cdoc2/cli/commands/CDocListCmd.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ public Void call() throws Exception {
8989
}
9090

9191
DecryptionKeyMaterial decryptionKm =
92-
SymmetricKeyUtil.extractDecryptionKeyMaterialFromSymmetricKey(
92+
SymmetricKeyUtil.extractDecryptionKeyMaterial(
9393
this.cdocFile.toPath(), this.password, this.secret
9494
);
9595

cdoc2-cli/src/main/java/ee/cyber/cdoc2/cli/commands/CDocReEncryptCmd.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ private EncryptionKeyMaterial extractSymmetricKeyEncKeyMaterial()
135135

136136
if (null != this.reEncryptPassword) {
137137
FormattedOptionParts splitPasswordAndLabel
138-
= SymmetricKeyUtil.getSplitPasswordAndLabel(this.reEncryptPassword);
138+
= SymmetricKeyUtil.getSplitPasswordAndLabel(this.reEncryptPassword, true);
139139
return SymmetricKeyUtil.extractEncryptionKeyMaterialFromPassword(splitPasswordAndLabel);
140140
}
141141

test/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@ This command will install helper libraries into temporary folder `/target` and a
1111
into specified release tags. The folder won't be commited and can be deleted manually after testing
1212
and recreated again running the command ones more.
1313

14+
#### Expect
15+
16+
For interactive tests, 'expect' tool is used. For Debian based system it can be installed with:
17+
```bash
18+
sudo apt-get install expect
19+
```
20+
If expect is not installed, then tests that require it, will be skipped.
1421

1522
## Tests running
1623

0 commit comments

Comments
 (0)