001// SPDX-FileCopyrightText: 2020 Paul Schaub <vanitasvitae@fsfe.org> 002// 003// SPDX-License-Identifier: Apache-2.0 004 005package sop.cli.picocli.commands; 006 007import java.io.File; 008import java.io.FileInputStream; 009import java.io.FileNotFoundException; 010import java.io.FileOutputStream; 011import java.io.IOException; 012import java.io.PrintWriter; 013import java.util.ArrayList; 014import java.util.Date; 015import java.util.List; 016import java.util.regex.Pattern; 017 018import picocli.CommandLine; 019import sop.DecryptionResult; 020import sop.ReadyWithResult; 021import sop.SessionKey; 022import sop.Verification; 023import sop.cli.picocli.DateParser; 024import sop.cli.picocli.FileUtil; 025import sop.cli.picocli.SopCLI; 026import sop.exception.SOPGPException; 027import sop.operation.Decrypt; 028import sop.util.HexUtil; 029 030@CommandLine.Command(name = "decrypt", 031 description = "Decrypt a message from standard input", 032 exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) 033public class DecryptCmd implements Runnable { 034 035 private static final String SESSION_KEY_OUT = "--session-key-out"; 036 private static final String VERIFY_OUT = "--verify-out"; 037 038 private static final String ERROR_UNSUPPORTED_OPTION = "Option '%s' is not supported."; 039 private static final String ERROR_FILE_NOT_EXIST = "File '%s' does not exist."; 040 private static final String ERROR_OUTPUT_OF_OPTION_EXISTS = "Target %s of option %s already exists."; 041 042 @CommandLine.Option( 043 names = {SESSION_KEY_OUT}, 044 description = "Can be used to learn the session key on successful decryption", 045 paramLabel = "SESSIONKEY") 046 File sessionKeyOut; 047 048 @CommandLine.Option( 049 names = {"--with-session-key"}, 050 description = "Enables decryption of the \"CIPHERTEXT\" using the session key directly against the \"SEIPD\" packet", 051 paramLabel = "SESSIONKEY") 052 List<String> withSessionKey = new ArrayList<>(); 053 054 @CommandLine.Option( 055 names = {"--with-password"}, 056 description = "Enables decryption based on any \"SKESK\" packets in the \"CIPHERTEXT\"", 057 paramLabel = "PASSWORD") 058 List<String> withPassword = new ArrayList<>(); 059 060 @CommandLine.Option(names = {VERIFY_OUT}, 061 description = "Produces signature verification status to the designated file", 062 paramLabel = "VERIFICATIONS") 063 File verifyOut; 064 065 @CommandLine.Option(names = {"--verify-with"}, 066 description = "Certificates whose signatures would be acceptable for signatures over this message", 067 paramLabel = "CERT") 068 List<File> certs = new ArrayList<>(); 069 070 @CommandLine.Option(names = {"--not-before"}, 071 description = "ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)\n" + 072 "Reject signatures with a creation date not in range.\n" + 073 "Defaults to beginning of time (\"-\").", 074 paramLabel = "DATE") 075 String notBefore = "-"; 076 077 @CommandLine.Option(names = {"--not-after"}, 078 description = "ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)\n" + 079 "Reject signatures with a creation date not in range.\n" + 080 "Defaults to current system time (\"now\").\n" + 081 "Accepts special value \"-\" for end of time.", 082 paramLabel = "DATE") 083 String notAfter = "now"; 084 085 @CommandLine.Parameters(index = "0..*", 086 description = "Secret keys to attempt decryption with", 087 paramLabel = "KEY") 088 List<File> keys = new ArrayList<>(); 089 090 @Override 091 public void run() { 092 throwIfOutputExists(verifyOut, VERIFY_OUT); 093 throwIfOutputExists(sessionKeyOut, SESSION_KEY_OUT); 094 095 Decrypt decrypt = SopCLI.getSop().decrypt(); 096 if (decrypt == null) { 097 throw new SOPGPException.UnsupportedSubcommand("Subcommand 'decrypt' not implemented."); 098 } 099 100 setNotAfter(notAfter, decrypt); 101 setNotBefore(notBefore, decrypt); 102 setWithPasswords(withPassword, decrypt); 103 setWithSessionKeys(withSessionKey, decrypt); 104 setVerifyWith(certs, decrypt); 105 setDecryptWith(keys, decrypt); 106 107 if (verifyOut != null && certs.isEmpty()) { 108 String errorMessage = "Option %s is requested, but no option %s was provided."; 109 throw new SOPGPException.IncompleteVerification(String.format(errorMessage, VERIFY_OUT, "--verify-with")); 110 } 111 112 try { 113 ReadyWithResult<DecryptionResult> ready = decrypt.ciphertext(System.in); 114 DecryptionResult result = ready.writeTo(System.out); 115 writeSessionKeyOut(result); 116 writeVerifyOut(result); 117 } catch (SOPGPException.BadData badData) { 118 throw new SOPGPException.BadData("No valid OpenPGP message found on Standard Input.", badData); 119 } catch (IOException ioException) { 120 throw new RuntimeException(ioException); 121 } 122 } 123 124 private void throwIfOutputExists(File outputFile, String optionName) { 125 if (outputFile == null) { 126 return; 127 } 128 129 if (outputFile.exists()) { 130 throw new SOPGPException.OutputExists(String.format(ERROR_OUTPUT_OF_OPTION_EXISTS, outputFile.getAbsolutePath(), optionName)); 131 } 132 } 133 134 private void writeVerifyOut(DecryptionResult result) throws IOException { 135 if (verifyOut != null) { 136 FileUtil.createNewFileOrThrow(verifyOut); 137 try (FileOutputStream outputStream = new FileOutputStream(verifyOut)) { 138 PrintWriter writer = new PrintWriter(outputStream); 139 for (Verification verification : result.getVerifications()) { 140 // CHECKSTYLE:OFF 141 writer.println(verification.toString()); 142 // CHECKSTYLE:ON 143 } 144 writer.flush(); 145 } 146 } 147 } 148 149 private void writeSessionKeyOut(DecryptionResult result) throws IOException { 150 if (sessionKeyOut != null) { 151 FileUtil.createNewFileOrThrow(sessionKeyOut); 152 153 try (FileOutputStream outputStream = new FileOutputStream(sessionKeyOut)) { 154 if (!result.getSessionKey().isPresent()) { 155 throw new SOPGPException.UnsupportedOption("Session key not extracted. Possibly the feature --session-key-out is not supported."); 156 } else { 157 SessionKey sessionKey = result.getSessionKey().get(); 158 outputStream.write(sessionKey.getAlgorithm()); 159 outputStream.write(sessionKey.getKey()); 160 } 161 } 162 } 163 } 164 165 private void setDecryptWith(List<File> keys, Decrypt decrypt) { 166 for (File key : keys) { 167 try (FileInputStream keyIn = new FileInputStream(key)) { 168 decrypt.withKey(keyIn); 169 } catch (SOPGPException.KeyIsProtected keyIsProtected) { 170 throw new SOPGPException.KeyIsProtected("Key in file " + key.getAbsolutePath() + " is password protected.", keyIsProtected); 171 } catch (SOPGPException.BadData badData) { 172 throw new SOPGPException.BadData("File " + key.getAbsolutePath() + " does not contain a private key.", badData); 173 } catch (FileNotFoundException e) { 174 throw new SOPGPException.MissingInput(String.format(ERROR_FILE_NOT_EXIST, key.getAbsolutePath()), e); 175 } catch (IOException e) { 176 throw new RuntimeException(e); 177 } 178 } 179 } 180 181 private void setVerifyWith(List<File> certs, Decrypt decrypt) { 182 for (File cert : certs) { 183 try (FileInputStream certIn = new FileInputStream(cert)) { 184 decrypt.verifyWithCert(certIn); 185 } catch (FileNotFoundException e) { 186 throw new SOPGPException.MissingInput(String.format(ERROR_FILE_NOT_EXIST, cert.getAbsolutePath()), e); 187 } catch (SOPGPException.BadData badData) { 188 throw new SOPGPException.BadData("File " + cert.getAbsolutePath() + " does not contain a valid certificate.", badData); 189 } catch (IOException ioException) { 190 throw new RuntimeException(ioException); 191 } 192 } 193 } 194 195 private void setWithSessionKeys(List<String> withSessionKey, Decrypt decrypt) { 196 Pattern sessionKeyPattern = Pattern.compile("^\\d+:[0-9A-F]+$"); 197 for (String sessionKey : withSessionKey) { 198 if (!sessionKeyPattern.matcher(sessionKey).matches()) { 199 throw new IllegalArgumentException("Session keys are expected in the format 'ALGONUM:HEXKEY'."); 200 } 201 String[] split = sessionKey.split(":"); 202 byte algorithm = (byte) Integer.parseInt(split[0]); 203 byte[] key = HexUtil.hexToBytes(split[1]); 204 205 try { 206 decrypt.withSessionKey(new SessionKey(algorithm, key)); 207 } catch (SOPGPException.UnsupportedOption unsupportedOption) { 208 throw new SOPGPException.UnsupportedOption(String.format(ERROR_UNSUPPORTED_OPTION, "--with-session-key"), unsupportedOption); 209 } 210 } 211 } 212 213 private void setWithPasswords(List<String> withPassword, Decrypt decrypt) { 214 for (String password : withPassword) { 215 try { 216 decrypt.withPassword(password); 217 } catch (SOPGPException.UnsupportedOption unsupportedOption) { 218 throw new SOPGPException.UnsupportedOption(String.format(ERROR_UNSUPPORTED_OPTION, "--with-password"), unsupportedOption); 219 } 220 } 221 } 222 223 private void setNotAfter(String notAfter, Decrypt decrypt) { 224 Date notAfterDate = DateParser.parseNotAfter(notAfter); 225 try { 226 decrypt.verifyNotAfter(notAfterDate); 227 } catch (SOPGPException.UnsupportedOption unsupportedOption) { 228 throw new SOPGPException.UnsupportedOption(String.format(ERROR_UNSUPPORTED_OPTION, "--not-after"), unsupportedOption); 229 } 230 } 231 232 private void setNotBefore(String notBefore, Decrypt decrypt) { 233 Date notBeforeDate = DateParser.parseNotBefore(notBefore); 234 try { 235 decrypt.verifyNotBefore(notBeforeDate); 236 } catch (SOPGPException.UnsupportedOption unsupportedOption) { 237 throw new SOPGPException.UnsupportedOption(String.format(ERROR_UNSUPPORTED_OPTION, "--not-before"), unsupportedOption); 238 } 239 } 240}