Created
April 14, 2020 08:48
-
-
Save WuglyakBolgoink/e10533849dc2e8fb50596c1da3f8142c to your computer and use it in GitHub Desktop.
Generate Android Signing Certificates
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env node | |
'use strict'; | |
// ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- -- | |
// Script to create android signing keys | |
// ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- -- | |
// Dependencies: | |
// - pwgen (PWGen is a professional password generator capable of creating large amounts of cryptographically-secure passwords) | |
// - keytool (The keytool command stores the keys and certificates in a keystore) | |
// ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- -- | |
// Usage: | |
// $ node generate_android_signing_certificate.js --app=myapp --release=debug --dname-o=COMPANY --dname-ou=IT --dname-l=CITY --dname-st=STATE --dname-c=LAND_CODE --output=./.tmp/myapp-android-keys | |
// ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- -- | |
// alias => <appName>-<appRelease> => myapp-debug || myapp-release | |
// dname.commonName => `publisher-${settings.appName}` => publisher-myapp | |
// dname => CN=<dname.commonName> OU=<dname.organizationalUnit> O=<dname.organization> L=<dname.location> ST=<dname.state> C=<dname.country> | |
// ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- -- | |
// ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- -- | |
// To Do: | |
// - keytool should be checked before start script | |
// - replace generated password with base64 string to avoid problems with special characters | |
// ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- -- | |
const fs = require('fs-extra'); | |
const path = require('path'); | |
const log = require('fancy-log'); | |
const $yargs = require('yargs'); | |
const moment = require('moment'); | |
const forge = require('node-forge'); | |
const _ = require('lodash'); | |
const {execSync: shell} = require('child_process'); | |
/** | |
* @type {SigningInputArgs} | |
*/ | |
const {argv: args} = $yargs; | |
const VALIDITY_IN_DAYS = 10000; | |
// ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- -- | |
/** | |
* @type {SigningSettings} | |
*/ | |
const signingSettings = _prepareSettings(args); | |
log('Signing settings:', JSON.stringify(signingSettings, null, 2)); | |
// ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- -- | |
_prepareKeystoreFolder(); | |
// Create JSK keystore | |
_runKeytool('JKS'); | |
// Fix warning: The JKS keystore uses a proprietary format. It is recommended to migrate to PKCS12 which is an industry standard format using "keytool -importkeystore -srckeystore mywebsite.jks -destkeystore mywebsite.jks -deststoretype pkcs12" | |
_runKeytool('PKCS12'); | |
_savePasswordsIntoBackupFile(); | |
// ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- -- | |
/** | |
* @param {SigningInputArgs} args - Input arguments. | |
* @return {SigningSettings} | |
* @private | |
*/ | |
function _prepareSettings(args) { | |
/** | |
* @type {CommandArgs} | |
*/ | |
const commandArgs = { | |
appName: _.get(args, 'app', null), | |
appRelease: _.get(args, 'release', null), | |
verbose: _.get(args, 'verbose', false), | |
validity: _.get(args, 'validity', VALIDITY_IN_DAYS), | |
dname: { | |
commonName: '', | |
organizationalUnit: _.get(args, 'dnameOu', ''), | |
organization: _.get(args, 'dnameO', ''), | |
location: _.get(args, 'dnameL', ''), | |
state: _.get(args, 'dnameSt', ''), | |
country: _.get(args, 'dnameC', '') | |
}, | |
outputFolder: _.get(args, 'output', __dirname) | |
}; | |
if (!_isValidInputParameters(commandArgs)) { | |
log.error('Error: Wrong configuration. Please check input parameters!'); | |
process.exit(1); | |
} | |
const currentTime = moment(); | |
/** | |
* @type {SigningSettings} | |
*/ | |
const result = { | |
...commandArgs, | |
alias: `${commandArgs.appName}-${commandArgs.appRelease}`, | |
currentTime: currentTime.format('DDMMYYYY_HHmmss'), | |
// create random passwords | |
keypass: _generateRandomPassword(), | |
storepass: _generateRandomPassword(), | |
dname: { | |
...commandArgs.dname, | |
commonName: `publisher-${commandArgs.appName}` | |
}, | |
distinguishedName: '', | |
keysFolder: '', | |
keystore: '', | |
keystorePKCS12: '', | |
pwBackupFile: '' | |
}; | |
result.distinguishedName = [ | |
`CN=${result.dname.commonName}`, | |
`OU=${result.dname.organizationalUnit}`, | |
`O=${result.dname.organization}`, | |
`L=${result.dname.location}`, | |
`ST=${result.dname.state}`, | |
`C=${result.dname.country}` | |
].join(';'); | |
result.keysFolder = path.join(result.outputFolder, `/android_keys/${result.alias}_${result.currentTime}`); | |
// Keystore files | |
result.keystore = path.join(result.keysFolder, `${result.appName}-${result.appRelease}-key.jks`); | |
result.keystorePKCS12 = path.join(result.keysFolder, `${result.appName}-${result.appRelease}-key.p12`); | |
// Save generated passwords into "backup" file | |
result.pwBackupFile = path.join(result.keysFolder, `${result.alias}-passwords.txt`); | |
return result; | |
} | |
/** | |
* @param {string} storetype - Should be 'JKS' or 'PKCS12'. | |
* @private | |
*/ | |
function _runKeytool(storetype) { | |
if (_.isNil(storetype) || !_.isString(storetype)) { | |
log.error('Error: Undefined storetype'); | |
process.exit(1); | |
} | |
let params = []; | |
switch (storetype) { | |
case 'JKS': { | |
const {keystore, storepass, keypass, alias, distinguishedName, validity, verbose} = signingSettings; | |
log(`Creating the sign key for [${alias}]...`); | |
params = [ | |
'-genkey', | |
'-noprompt', | |
`-alias '${alias}'`, | |
`-keyalg RSA`, | |
`-keysize 4096`, | |
`-dname '${distinguishedName}'`, | |
`-validity ${validity}`, | |
`-keypass '${keypass}'`, | |
`-keystore '${keystore}'`, | |
`-storetype JKS`, | |
`-storepass '${storepass}'` | |
]; | |
if (verbose) { | |
params.push('-v'); | |
} | |
_executeKeytoolCommand(storetype, params, keystore); | |
break; | |
} | |
case 'PKCS12': { | |
const {keystore, keystorePKCS12, storepass, keypass, alias, verbose} = signingSettings; | |
log(`Convert JKS keystore [${keystore}] into PKCS12 format...`); | |
params = [ | |
'-importkeystore', | |
'-noprompt', | |
`-srckeystore '${keystore}'`, | |
`-destkeystore '${keystorePKCS12}'`, | |
`-deststoretype pkcs12`, | |
`-srcalias '${alias}'`, | |
`-destalias '${alias}'`, | |
`-deststorepass '${storepass}'`, | |
`-srcstorepass '${storepass}'`, | |
`-srckeypass '${keypass}'`, | |
`-destkeypass '${storepass}'` | |
]; | |
if (verbose) { | |
params.push('-v'); | |
} | |
_executeKeytoolCommand(storetype, params, keystorePKCS12); | |
break; | |
} | |
default: { | |
log.error('Error: Unknown storetype'); | |
process.exit(1); | |
break; | |
} | |
} | |
} | |
function _executeKeytoolCommand(storetype, params, keystoreFile) { | |
try { | |
const cmd = [ | |
`keytool`, | |
...params | |
].join(' '); | |
log(cmd); | |
shell(cmd); | |
shell(`shasum --algorithm 512 --binary ${keystoreFile} >> ${keystoreFile}.checksum.txt`); | |
log('Done'); | |
} catch (e) { | |
log.error(`Error[${storetype}]`, e.message); | |
// todo: maybe we can use this for debugging!? | |
if (_.has(e, 'stderr')) { | |
log.error('e.stderr:' + e.stderr.toString('utf8')); | |
} | |
if (_.has(e, 'output')) { | |
log.error('e.output:' + e.output.toString('utf8')); | |
} | |
process.exit(1); | |
} | |
} | |
/** | |
* @return {string} | |
* @private | |
*/ | |
function _generateRandomPassword() { | |
/** | |
* pwgen [ OPTION ] [ pw_length ] [ num_pw ] | |
* -1 - Print the generated passwords one per line. | |
* -N, --num-passwords=num - Generate num passwords. This defaults to a screenful if passwords are printed by columns, and one password otherwise. | |
* -r chars, --remove-chars=chars - Don't use the specified characters in password. This option will disable the phomeme-based generator and uses the random password generator. | |
* -s, --secure - Generate completely random, hard-to-memorize passwords. These should only be used for machine passwords, since otherwise it's almost guaranteed that users will simply write the password on a piece of paper taped to the monitor... | |
* -y, --symbols - Include at least one special character in the password. | |
* -c, --capitalize - Include at least one capital letter in the password. This is the default if the standard output is a tty device. | |
*/ | |
const passwordAsBuffer = shell('pwgen --secure --symbols --capitalize --remove-chars="\\\ \\`\\"\'" 64 1'); | |
return _.trim(passwordAsBuffer.toString('utf8')); | |
} | |
/** | |
* @description Validate required input parameters. | |
* @param {CommandArgs} cmdArgs - Settings object. | |
* @return {boolean} | |
* @private | |
*/ | |
function _isValidInputParameters(cmdArgs) { | |
const ck1 = _.has(cmdArgs, 'appName') && !_.isNil(cmdArgs.appName) && !_.isEmpty(cmdArgs.appName); | |
const ck2 = _.has(cmdArgs, 'appRelease') && !_.isNil(cmdArgs.appRelease) && !_.isEmpty(cmdArgs.appRelease); | |
const ck3 = _.has(cmdArgs, 'dname.organizationalUnit') && !_.isEmpty(cmdArgs.dname.organizationalUnit) && !_.isEmpty(cmdArgs.dname.organizationalUnit); | |
const ck4 = _.has(cmdArgs, 'dname.organization') && !_.isEmpty(cmdArgs.dname.organization) && !_.isEmpty(cmdArgs.dname.organization); | |
const ck5 = _.has(cmdArgs, 'dname.location') && !_.isEmpty(cmdArgs.dname.location) && !_.isEmpty(cmdArgs.dname.location); | |
const ck6 = _.has(cmdArgs, 'dname.state') && !_.isEmpty(cmdArgs.dname.state) && !_.isEmpty(cmdArgs.dname.state); | |
const ck7 = _.has(cmdArgs, 'dname.country') && !_.isEmpty(cmdArgs.dname.country) && !_.isEmpty(cmdArgs.dname.country); | |
return ck1 && ck2 && ck3 && ck4 && ck5 && ck6 && ck7; | |
} | |
/** | |
* @private | |
*/ | |
function _savePasswordsIntoBackupFile() { | |
try { | |
fs.outputFileSync( | |
signingSettings.pwBackupFile, | |
[ | |
`Schlüsselkennwort (-keypass) => Keystore-Kennwort (-storepass)`, | |
`${signingSettings.keypass} => ${signingSettings.storepass}` | |
].join('\n') | |
); | |
shell(`shasum --algorithm 512 --text ${signingSettings.pwBackupFile} >> ${signingSettings.pwBackupFile}.checksum.txt`); | |
} catch (e) { | |
log.error('Error: Can not save passwords into backup file.', e.message); | |
process.exit(1); | |
} | |
} | |
function _prepareKeystoreFolder() { | |
try { | |
// create keysFolder if neccessary | |
fs.ensureDirSync(signingSettings.keysFolder); | |
} catch (e) { | |
log.error(`Can not create [${signingSettings.keysFolder}] folder.`, e.message); | |
process.exit(1); | |
} | |
try { | |
if (fs.existsSync(signingSettings.keystore)) { | |
fs.removeSync(signingSettings.keystore); | |
} | |
if (fs.existsSync(signingSettings.keystorePKCS12)) { | |
fs.removeSync(signingSettings.keystorePKCS12); | |
} | |
} catch (e) { | |
log.error(`Can not clean keystore files. Error: ${e.message}`); | |
process.exit(1); | |
} | |
} | |
/** | |
* @typedef {Object} SigningSettings | |
* @property {string} keypass | |
* @property {string} pwBackupFile | |
* @property {string} keystorePKCS12 | |
* @property {string} appName | |
* @property {string} appRelease | |
* @property {string} storepass | |
* @property {string} distinguishedName | |
* @property {string} keysFolder | |
* @property {string} outputFolder | |
* @property {boolean} verbose | |
* @property {string} currentTime | |
* @property {string} alias | |
* @property {string} keystore | |
* @property {number} validity | |
* @property {SigningSettingsDName} dname | |
*/ | |
/** | |
* @typedef {Object} SigningSettingsDName | |
* @property {string} commonName | |
* @property {string} country | |
* @property {string} organization | |
* @property {string} location | |
* @property {string} state | |
* @property {string} organizationalUnit | |
*/ | |
/** | |
* @typedef {Object} CommandArgs | |
* @property {string} appName | |
* @property {string} appRelease | |
* @property {boolean} verbose | |
* @property {number} validity | |
* @property {SigningSettingsDName} dname | |
* @property {string} outputFolder | |
*/ | |
/** | |
* @typedef {Object} SigningInputArgs | |
* @property {string} app - Application name. | |
* @property {string} release - Release version [debug, release]. | |
* @property {boolean} verbose - Verbose output (by default 10.000 days). | |
* @property {number} validity - Validity period in days (Important: Your application must be signed with a cryptographic key whose validity period ends after 22 October 2033(https://developer.android.com/studio/publish/preparing)). | |
* @property {string} dnameOu - OrganizationalUnit. | |
* @property {string} dnameO - Organization. | |
* @property {string} dnameL - Location. | |
* @property {string} dnameSt - State. | |
* @property {string} dnameC - Country. | |
* @property {string} output - Output folder (by default - current dir). | |
*/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment