Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save WuglyakBolgoink/e10533849dc2e8fb50596c1da3f8142c to your computer and use it in GitHub Desktop.
Save WuglyakBolgoink/e10533849dc2e8fb50596c1da3f8142c to your computer and use it in GitHub Desktop.
Generate Android Signing Certificates
#!/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