UNPKG

10.8 kBJavaScriptView Raw
1/**
2 * Certificate management tools.
3 *
4 * @module certs
5 *
6 * @copyright
7 * Copyright (c) 2014 by Appcelerator, Inc. All Rights Reserved.
8 *
9 * @license
10 * Licensed under the terms of the Apache Public License.
11 * Please see the LICENSE included with this distribution for details.
12 */
13
14const
15 appc = require('node-appc'),
16 fs = require('fs'),
17 magik = require('./utilities').magik,
18 moment = require('moment'),
19 path = require('path'),
20 visualstudio = require('./visualstudio'),
21 wrench = require('wrench'),
22 cp = require('child_process'),
23 exec = cp.exec,
24 __ = appc.i18n(__dirname).__;
25
26exports.create = create;
27exports.generate = generate;
28exports.generatePFX = generatePFX;
29exports.thumbprint = thumbprint;
30exports.test = {
31 parseCertUtilOutput: parseCertUtilOutput
32};
33
34
35/**
36 * Launches native dialogs for generating a certificate. This will create a cert and private key and then will convert them into a pfx file.
37 *
38 * @param {String} appid - The application's id.
39 * @param {String} certificateFile - The path where the certificate is to be created.
40 * @param {Object} [options] - An object containing various settings.
41 * @param {String} [options.powershell='powershell'] - Path to the 'powershell' executable.
42 * @param {String} [options.pvk2pfx='Pvk2Pfx'] - The path to the 'pvk2pfx' executable.
43 * @param {Function} [callback(err, certificateFile)] - A function to call after the cert has been created.
44 *
45 * @emits module:certs#created
46 * @emits module:certs#error
47 *
48 * @returns {EventEmitter}
49 */
50function create(appid, certificateFile, options, callback) {
51 return magik(options, callback, function (emitter, options, callback) {
52 if (typeof appid !== 'string' || !appid) {
53 var ex = new Error(__('Missing required "%s" argument', 'appid'));
54 emitter.emit('error', ex);
55 return callback(ex);
56 }
57
58 if (typeof certificateFile !== 'string' || !certificateFile) {
59 var ex = new Error(__('Missing required "%s" argument', 'certificateFile'));
60 emitter.emit('error', ex);
61 return callback(ex);
62 }
63
64 var certDir = path.dirname(certificateFile),
65 certPath = certificateFile.replace(/\..+$/, '');
66
67 if (!fs.existsSync(certDir)) {
68 wrench.mkdirSyncRecursive(certDir);
69 }
70
71 visualstudio.detect(options, function (err, results) {
72 if (err) {
73 emitter.emit('error', err);
74 return callback(err);
75 }
76
77 var vsInfo = results.selectedVisualStudio;
78
79 if (!vsInfo) {
80 var ex = new Error(__('Unable to find a supported Visual Studio installation'));
81 emitter.emit('error', ex);
82 return callback(ex);
83 }
84 // TODO Use generate method instead? There are very slight difference between the script and generate function
85 appc.subprocess.getRealName(path.resolve(__dirname, '..', 'bin', 'winstore_create_cert.ps1'), function (err, psScript) {
86 if (err) {
87 emitter.emit('error', err);
88 return callback(err);
89 }
90
91 // first lets create the cert
92 appc.subprocess.run(vsInfo.vsDevCmd.replace(/[ \(\)\&]/g, '^$&'), [
93 '&&',
94 options.powershell || 'powershell',
95 '-ExecutionPolicy', 'Bypass', '-NoLogo', '-NonInteractive', '-NoProfile',
96 '-File', psScript,
97 appid,
98 moment().add(2, 'years').format('L'),
99 '"' + certPath + '"'
100 ], function (code, out, err) {
101 if (code) {
102 var ex = new Error(__('Failed to create certificate (code %s)', code));
103 emitter.emit('error', ex);
104 return callback(ex);
105 }
106
107 generatePFX(certPath + '.pvk', certPath + '.cer', certPath + '.pfx', options, function(err, pfxFile) {
108 if (err) {
109 emitter.emit('error', err);
110 callback(err);
111 } else {
112 // Once generated, remove the private key and cert
113 fs.existsSync(certPath + '.pvk') && fs.unlinkSync(certPath + '.pvk');
114 fs.existsSync(certPath + '.cer') && fs.unlinkSync(certPath + '.cer');
115
116 emitter.emit('created', pfxFile);
117 callback(null, pfxFile);
118 }
119 });
120 });
121 });
122 });
123 });
124}
125
126/**
127 * Returns the thumbprint from certutil -dump output
128 *
129 * @param {String} certificateFile - The path where the certificate/pfx file lives.
130 * @param {String} out - The output from certutil
131 * @param {Function} [callback(err, thumbprint)] - A function to call after the thumbprint has been extracted.
132 */
133function parseCertUtilOutput(certificateFile, out, callback) {
134 if (out.indexOf('The system cannot find the file specified.') >= 0) {
135 callback(new Error('No certificate was found at the path: "' + certificateFile + '"'));
136 }
137 else {
138 try {
139 callback(null, out.split('(sha1): ')[1].split('\r')[0].split(' ').join('').toUpperCase());
140 } catch (E) {
141 callback(new Error('Unexpected output: ' + E.toString() + '\n' + out));
142 }
143 }
144}
145
146/**
147 * Returns the thumbprint for a certificate/PFX file
148 *
149 * @param {String} certificateFile - The path where the certificate/pfx file lives.
150 * @param {String} password - The password for the certificate/pfx file.
151 * @param {Function} [callback(err, thumbprint)] - A function to call after the thumbprint has been extracted.
152 */
153function thumbprint(certificateFile, password, callback) {
154 if (!password) {
155 password = '""';
156 }
157 exec('certutil -p ' + password + ' -dump "' + certificateFile + '"', function (code, out, err) {
158 if (code) {
159 return callback(err);
160 }
161 parseCertUtilOutput(certificateFile, out, callback);
162 });
163}
164
165/**
166 * Generates a self-signed cert and private key pair.
167 *
168 * @param {String} subjectName - The subject name to use in the cert.
169 * @param {String} destinationDir - The path where the certificate and private key will be created.
170 * @param {Object} [options] - An object containing various settings.
171 * @param {String} [options.makeCert='MakeCert'] - The path to the 'makecert' executable.
172 * @param {Function} [callback(err, privateKeyFile, certificateFile)] - A function to call after the private key and certificate have been created.
173 *
174 * @emits module:certs#created
175 * @emits module:certs#error
176 *
177 * @returns {EventEmitter}
178 */
179function generate(subjectName, certificateFile, options, callback) {
180 return magik(options, callback, function (emitter, options, callback) {
181 if (typeof subjectName !== 'string' || !subjectName) {
182 var ex = new Error(__('Missing required "%s" argument', 'subjectName'));
183 emitter.emit('error', ex);
184 return callback(ex);
185 }
186
187 if (typeof certificateFile !== 'string' || !certificateFile) {
188 var ex = new Error(__('Missing required "%s" argument', 'certificateFile'));
189 emitter.emit('error', ex);
190 return callback(ex);
191 }
192
193 var certDir = path.dirname(certificateFile),
194 certPath = certificateFile.replace(/\..+$/, '');
195
196 if (!fs.existsSync(certDir)) {
197 wrench.mkdirSyncRecursive(certDir);
198 }
199
200 visualstudio.detect(options, function (err, results) {
201 if (err) {
202 emitter.emit('error', err);
203 return callback(err);
204 }
205
206 var vsInfo = results.selectedVisualStudio,
207 pvk = certPath + '.pvk',
208 cer = certPath + '.cer',
209 expirationDate = moment().add(2, 'years').format('L'),
210 args = [];
211
212 if (!vsInfo) {
213 var ex = new Error(__('Unable to find a supported Visual Studio installation'));
214 emitter.emit('error', ex);
215 return callback(ex);
216 }
217
218 args = [
219 '&&', options.makeCert || 'MakeCert',
220 '-n', subjectName, '-r', '-h', '0', '-eku', '1.3.6.1.5.5.7.3.3,1.3.6.1.4.1.311.10.3.13',
221 '-e', expirationDate, '-sv', pvk, cer
222 ];
223
224 appc.subprocess.run(vsInfo.vsDevCmd.replace(/[ \(\)\&]/g, '^$&'), args, function (code, out, err) {
225 if (code) {
226 var ex = new Error(__('Failed to create certificate (code %s)', code));
227 emitter.emit('error', ex);
228 return callback(ex);
229 } else {
230 emitter.emit('created', certPath + '.cer');
231 callback(null, certPath + '.pvk', certPath + '.cer');
232 }
233 });
234 });
235 });
236}
237
238/**
239 * Launches native dialogs for generating a certificate.
240 *
241 * @param {String} privateKeyFile - The path where the private key (pvk) file lives.
242 * @param {String} certificateFile - The path where the certificate (cer) file lives.
243 * @param {String} pfxDestinationFile - The path where the PFX file will be created.
244 * @param {String} [password] - The password for the pfx
245 * @param {Object} [options] - An object containing various settings.
246 * @param {String} [options.powershell='powershell'] - Path to the 'powershell' executable.
247 * @param {String} [options.pvk2pfx='Pvk2Pfx'] - The path to the 'pvk2pfx' executable.
248 * @param {Function} [callback(err, pfxDestinationFile)] - A function to call after the PFX has been created.
249 *
250 * @emits module:certs#created
251 * @emits module:certs#error
252 *
253 * @returns {EventEmitter}
254 */
255function generatePFX(privateKeyFile, certificateFile, pfxDestinationFile, password, options, callback) {
256 return magik(options, callback, function (emitter, options, callback) {
257 if (typeof privateKeyFile !== 'string' || !privateKeyFile) {
258 var ex = new Error(__('Missing required "%s" argument', 'privateKeyFile'));
259 emitter.emit('error', ex);
260 return callback(ex);
261 }
262
263 if (typeof certificateFile !== 'string' || !certificateFile) {
264 var ex = new Error(__('Missing required "%s" argument', 'certificateFile'));
265 emitter.emit('error', ex);
266 return callback(ex);
267 }
268
269 if (typeof pfxDestinationFile !== 'string' || !pfxDestinationFile) {
270 var ex = new Error(__('Missing required "%s" argument', 'pfxDestinationFile'));
271 emitter.emit('error', ex);
272 return callback(ex);
273 }
274
275 var destDir = path.dirname(pfxDestinationFile);
276
277 if (!fs.existsSync(destDir)) {
278 wrench.mkdirSyncRecursive(destDir);
279 }
280
281 visualstudio.detect(options, function (err, results) {
282 if (err) {
283 emitter.emit('error', err);
284 return callback(err);
285 }
286
287 var vsInfo = results.selectedVisualStudio,
288 args = [];
289
290 if (!vsInfo) {
291 var ex = new Error(__('Unable to find a supported Visual Studio installation'));
292 emitter.emit('error', ex);
293 return callback(ex);
294 }
295
296 args = [
297 '&&', options.pvk2pfx || 'Pvk2Pfx',
298 '/pvk', privateKeyFile,
299 '/spc', certificateFile,
300 '/pfx', pfxDestinationFile
301 ];
302 if (password && password !== '') {
303 args.push('/pi');
304 args.push(password);
305 }
306
307 // package the certificate as pfx
308 appc.subprocess.run(vsInfo.vsDevCmd.replace(/[ \(\)\&]/g, '^$&'), args, function (code, out, err) {
309 if (code) {
310 var ex = new Error(__('Failed to convert certificate to pfx (code %s)', code));
311 emitter.emit('error', ex);
312 callback(ex);
313 } else {
314 emitter.emit('created', pfxDestinationFile);
315 callback(null, pfxDestinationFile);
316 }
317 });
318 });
319 });
320}