Skip to content

Commit e46c55f

Browse files
author
Greg Meyer
authored
Merge pull request #13 from DirectProjectJavaRI/develop
Pulling in fixes to notification message generation.
2 parents acfa521 + 4f8e00b commit e46c55f

4 files changed

Lines changed: 466 additions & 6 deletions

File tree

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<modelVersion>4.0.0</modelVersion>
55
<artifactId>agent</artifactId>
66
<name>Direct Project Security And Trust Agent</name>
7-
<version>6.0.3</version>
7+
<version>6.0.4-SNAPSHOT</version>
88
<description>Direct Project Security And Trust Agent</description>
99
<inceptionYear>2010</inceptionYear>
1010
<url>https://github.com/DirectProjectJavaRI/agent</url>
Lines changed: 350 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,350 @@
1+
/*
2+
Copyright (c) 2010, NHIN Direct Project
3+
All rights reserved.
4+
5+
Authors:
6+
Greg Meyer gm2552@cerner.com
7+
8+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
9+
10+
Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
11+
Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer
12+
in the documentation and/or other materials provided with the distribution. Neither the name of the The NHIN Direct Project (nhindirect.org).
13+
nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
14+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15+
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS
16+
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
17+
GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
18+
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
19+
THE POSSIBILITY OF SUCH DAMAGE.
20+
*/
21+
22+
package org.nhindirect.stagent.cert.tools;
23+
24+
///CLOVER:OFF
25+
import java.io.ByteArrayInputStream;
26+
import java.io.File;
27+
import java.io.FileOutputStream;
28+
import java.security.Key;
29+
import java.security.KeyFactory;
30+
import java.security.KeyStore;
31+
import java.security.PrivateKey;
32+
import java.security.cert.X509Certificate;
33+
import java.security.spec.PKCS8EncodedKeySpec;
34+
import java.util.Enumeration;
35+
36+
import javax.crypto.EncryptedPrivateKeyInfo;
37+
import javax.crypto.SecretKey;
38+
import javax.crypto.SecretKeyFactory;
39+
import javax.crypto.spec.PBEKeySpec;
40+
41+
import org.apache.commons.io.FileUtils;
42+
import org.apache.commons.io.IOUtils;
43+
import org.nhindirect.common.crypto.CryptoExtensions;
44+
import org.nhindirect.stagent.NHINDException;
45+
import org.nhindirect.stagent.cert.X509CertificateEx;
46+
47+
/**
48+
* Application class for removing password and private key passphrase protection from PKCS12 files for importing into the Direct Project
49+
* configuration UI.
50+
*
51+
* @author Greg Meyer
52+
*/
53+
public class ChangeP12Passphrase
54+
{
55+
private static File p12File;
56+
private static String filePassPhrase = "";
57+
private static String keyPassPhrase = "";
58+
private static File createFile;
59+
private static String newPassPhrase = "";
60+
61+
/*
62+
* Load BC the JCS provider.
63+
*/
64+
static
65+
{
66+
CryptoExtensions.registerJCEProviders();
67+
}
68+
69+
/**
70+
* Main entry point when running as an application. Use the -help option for usage.
71+
* @param argv Application arguments.
72+
*/
73+
public static void main (String[] argv)
74+
{
75+
if (argv.length == 0)
76+
{
77+
System.err.println("Invalid number of arguments: can't have 0 arguments.");
78+
printUsage();
79+
System.exit(-1);
80+
}
81+
82+
// Check parameters
83+
for (int i = 0; i < argv.length; i++)
84+
{
85+
String arg = argv[i];
86+
87+
// Options
88+
if (!arg.startsWith("-"))
89+
{
90+
System.err.println("Error: Unexpected argument [" + arg + "]\n");
91+
printUsage();
92+
System.exit(-1);
93+
}
94+
else if (arg.equalsIgnoreCase("-p12"))
95+
{
96+
if (i == argv.length - 1 || argv[i + 1].startsWith("-"))
97+
{
98+
System.err.println("Error: p12 file name.");
99+
System.exit(-1);
100+
}
101+
102+
p12File = new File(argv[++i]);
103+
104+
}
105+
else if (arg.equals("-filePass"))
106+
{
107+
if (i == argv.length - 1 || argv[i + 1].startsWith("-"))
108+
{
109+
System.err.println("Error: Missing p12 file passphrase.");
110+
System.exit(-1);
111+
}
112+
filePassPhrase = argv[++i];
113+
}
114+
else if (arg.equals("-keyPass"))
115+
{
116+
if (i == argv.length - 1 || argv[i + 1].startsWith("-"))
117+
{
118+
System.err.println("Error: Missing private key passphrase.");
119+
System.exit(-1);
120+
}
121+
keyPassPhrase = argv[++i];
122+
}
123+
else if (arg.equals("-out"))
124+
{
125+
if (i == argv.length - 1 || argv[i + 1].startsWith("-"))
126+
{
127+
System.err.println("Error: Missing output file.");
128+
System.exit(-1);
129+
}
130+
createFile = new File(argv[++i]);
131+
}
132+
else if (arg.equals("-outPassword"))
133+
{
134+
if (i == argv.length - 1 || argv[i + 1].startsWith("-"))
135+
{
136+
System.err.println("Error: Missing output password.");
137+
System.exit(-1);
138+
}
139+
newPassPhrase = argv[++i];
140+
}
141+
else if (arg.equals("-help"))
142+
{
143+
printUsage();
144+
System.exit(-1);
145+
}
146+
else
147+
{
148+
System.err.println("Error: Unknown argument " + arg + "\n");
149+
printUsage();
150+
System.exit(-1);
151+
}
152+
}
153+
154+
if (validateParameters())
155+
{
156+
stripP12File();
157+
}
158+
System.exit(0);
159+
}
160+
161+
/*
162+
* Main strip operation of removing the password and passphrase and creating a new p12 file.
163+
*/
164+
@SuppressWarnings("deprecation")
165+
private static void stripP12File()
166+
{
167+
FileOutputStream outStr = null;
168+
try
169+
{
170+
byte[] p12Data = loadFileData(p12File);
171+
172+
if (p12Data != null)
173+
{
174+
X509CertificateEx p12Cert = certFromData(p12Data);
175+
if (p12Cert == null)
176+
return;
177+
178+
File outFile = getPKCS12OutFile();
179+
180+
181+
KeyStore localKeyStore = KeyStore.getInstance("PKCS12", CryptoExtensions.getJCEProviderName());
182+
localKeyStore.load(null, null);
183+
184+
185+
KeyFactory kf = KeyFactory.getInstance("RSA", CryptoExtensions.getJCEProviderName());
186+
PKCS8EncodedKeySpec keysp = null;
187+
if (keyPassPhrase != null && !keyPassPhrase.isEmpty())
188+
{
189+
byte[] keyData = p12Cert.getPrivateKey().getEncoded();
190+
EncryptedPrivateKeyInfo encInfo = new EncryptedPrivateKeyInfo(keyData);
191+
PBEKeySpec keySpec = new PBEKeySpec(keyPassPhrase.toCharArray());
192+
String alg = encInfo.getAlgName();
193+
194+
SecretKeyFactory secFactory = SecretKeyFactory.getInstance(alg, CryptoExtensions.getJCEProviderName());
195+
SecretKey secKey = secFactory.generateSecret(keySpec);
196+
keysp = encInfo.getKeySpec(secKey, CryptoExtensions.getJCEProviderName());
197+
}
198+
else
199+
{
200+
keysp = new PKCS8EncodedKeySpec ( p12Cert.getPrivateKey().getEncoded() );
201+
}
202+
203+
Key newPrivKey = kf.generatePrivate (keysp);
204+
205+
localKeyStore.setKeyEntry("privCert", newPrivKey, newPassPhrase.toCharArray(), new java.security.cert.Certificate[] {p12Cert});
206+
207+
208+
outStr = new FileOutputStream(outFile);
209+
localKeyStore.store(outStr, newPassPhrase.toCharArray());
210+
211+
System.out.println("Created pcks12 file " + createFile.getAbsolutePath());
212+
}
213+
}
214+
catch (Exception e)
215+
{
216+
System.out.println("Could not create p12 file " + e.getMessage());
217+
}
218+
finally
219+
{
220+
IOUtils.closeQuietly(outStr);
221+
}
222+
}
223+
224+
/*
225+
* Valid program parameters
226+
*/
227+
private static boolean validateParameters()
228+
{
229+
230+
if (p12File == null)
231+
{
232+
System.out.println("Missing input p12 file name");
233+
return false;
234+
}
235+
if (!p12File.exists())
236+
{
237+
System.out.println("P12 file " + p12File.getAbsolutePath() + " does not exist.");
238+
return false;
239+
}
240+
241+
return true;
242+
}
243+
244+
/*
245+
* Loads the raw data from the provided file to a byte array.
246+
*/
247+
private static byte[] loadFileData(File file) throws Exception
248+
{
249+
return FileUtils.readFileToByteArray(file);
250+
}
251+
252+
/*
253+
* Creates the output file descriptor and creates the new file on the file system.
254+
*/
255+
private static File getPKCS12OutFile() throws Exception
256+
{
257+
if (createFile == null)
258+
{
259+
260+
String fileName = p12File.getName();
261+
262+
int index = fileName.lastIndexOf(".");
263+
if (index > -1)
264+
fileName = fileName.substring(0, index);
265+
266+
fileName += "_newpass.p12";
267+
createFile = new File(fileName);
268+
}
269+
270+
if (createFile.exists())
271+
createFile.delete();
272+
273+
createFile.createNewFile();
274+
275+
return createFile;
276+
}
277+
278+
/*
279+
* Load the exiting p12 file using the provided password and private key passphrase.
280+
*/
281+
@SuppressWarnings("deprecation")
282+
private static X509CertificateEx certFromData(byte[] data)
283+
{
284+
X509CertificateEx retVal = null;
285+
try
286+
{
287+
ByteArrayInputStream bais = new ByteArrayInputStream(data);
288+
289+
// lets try this a as a PKCS12 data stream first
290+
try
291+
{
292+
KeyStore localKeyStore = KeyStore.getInstance("PKCS12", CryptoExtensions.getJCEProviderName());
293+
294+
localKeyStore.load(bais, filePassPhrase.toCharArray());
295+
Enumeration<String> aliases = localKeyStore.aliases();
296+
297+
298+
// we are really expecting only one alias
299+
if (aliases.hasMoreElements())
300+
{
301+
String alias = aliases.nextElement();
302+
X509Certificate cert = (X509Certificate)localKeyStore.getCertificate(alias);
303+
304+
// check if there is private key
305+
Key key = localKeyStore.getKey(alias, keyPassPhrase.toCharArray());
306+
if (key != null && key instanceof PrivateKey)
307+
{
308+
retVal = X509CertificateEx.fromX509Certificate(cert, (PrivateKey)key);
309+
}
310+
311+
}
312+
}
313+
catch (Exception e)
314+
{
315+
// must not be a PKCS12 stream, go on to next step
316+
System.out.println("Error decoding p12 input file: " + e.getMessage());
317+
}
318+
319+
IOUtils.closeQuietly(bais);
320+
}
321+
catch (Exception e)
322+
{
323+
throw new NHINDException("Data cannot be converted to a valid X.509 Certificate", e);
324+
}
325+
326+
return retVal;
327+
}
328+
329+
/*
330+
* Print program usage.
331+
*/
332+
private static void printUsage()
333+
{
334+
StringBuffer use = new StringBuffer();
335+
use.append("Usage:\n");
336+
use.append("java StripP12Passphrase (options)...\n\n");
337+
use.append("options:\n");
338+
use.append("-p12 p12 File P12 formatted file to strip the passphrase from.\n");
339+
use.append("\n");
340+
use.append("-filePass File passphrase Optional file passphrase protecting the p12 file.\n");
341+
use.append(" Default: \"\"\n\n");
342+
use.append("-keyPass Key passphrase Optional private key passphrase protecting the internal private key.\n");
343+
use.append(" Default: \"\"\n\n");
344+
use.append("-out Out File Optional output file name.\n");
345+
use.append(" Default: <p12 file>_nopass.p12\n\n");
346+
347+
System.err.println(use);
348+
}
349+
}
350+
///CLOVER:ON

src/main/java/org/nhindirect/stagent/mail/notifications/NotificationHelper.java

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
2525
import java.util.ArrayList;
2626
import java.util.Collection;
2727
import java.util.Collections;
28+
import java.util.List;
29+
import java.util.TreeSet;
2830
import java.util.Arrays;
2931

3032
import javax.mail.MessagingException;
@@ -107,11 +109,23 @@ public static String getNotificationDestination(Message message)
107109

108110
try
109111
{
110-
retVal = message.getHeader(MDNStandard.Headers.DispositionNotificationTo, ",");
111-
112-
if (retVal == null || retVal.isEmpty())
113-
{
114-
retVal = message.getHeader(MDNStandard.Headers.From, ",");
112+
String[] destinations = message.getHeader(MDNStandard.Headers.DispositionNotificationTo);
113+
114+
if (destinations == null || destinations.length == 0) {
115+
destinations = message.getHeader(MDNStandard.Headers.From);
116+
}
117+
118+
if (destinations == null || destinations.length == 0) {
119+
retVal = "";
120+
} else if (destinations.length == 1) {
121+
retVal = destinations[0];
122+
} else if (destinations.length > 1) {
123+
// remove (case-insensitive) duplicates
124+
List<String> uniqueDestinations = new ArrayList<>();
125+
Collections.addAll(uniqueDestinations, destinations);
126+
TreeSet<String> seen = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
127+
uniqueDestinations.removeIf(s -> !seen.add(s));
128+
retVal = String.join(",", uniqueDestinations);
115129
}
116130
}
117131
catch (MessagingException e) {/* no-op */}

0 commit comments

Comments
 (0)