/**
* Command line script - cryptography operations (mostly for UA)
* Usage (shut down server before usage):
* ubcli crypt --help
*
* @author pavel.mash
* @module crypt
* @memberOf module:@unitybase/ubcli
*/
const base = require('@unitybase/base')
const path = require('path')
const fs = require('fs')
const options = base.options
const argv = base.argv
let iitCrypto
let iitSettings
module.exports = function crypt () {
if (options.switchIndex('?') !== -1 || options.switchIndex('help') !== -1 ||
options.switchIndex('-help') !== -1) {
showUsage()
return
}
const serverConfig = argv.getServerConfiguration()
try {
iitCrypto = require('@ub-d/iit-crypto')
} catch (e) {
console.error('to use ubcli crypt module @ub-d/iit-crypto must be added into application')
throw e
}
iitSettings = serverConfig.security.dstu && serverConfig.security.dstu.iit
let iitSettingsPath = '/var/opt/unitybase/shared'
if (iitSettings && iitSettings.librarySettings) {
iitSettingsPath = iitSettings.librarySettings
}
iitCrypto.initForCommandLineUsage(iitSettingsPath) // NOTE - this required only for command line script. Inside server library init automatically
const [, , , command, arg1, arg2] = process.argv
const COMMANDS = {
enumDev,
sign,
signAsic,
verify,
hash,
split,
combine,
certParse
}
if (!COMMANDS[command]) {
if (command) console.error(`Unknown command ${command}`)
showUsage()
return
}
COMMANDS[command](arg1, arg2)
}
module.exports.shortDoc = "Cryptography operations. Run 'ubcli crypt --help' for usage"
/**
* Display usage
*/
function showUsage () {
console.log(`
Cryptography operations, expects '@ub-d/iit-crypto' package to be installed
ubcli crypt [command] [args]
Commands:
sign fileName4Sign signFormat [-k privateKeyPath] [-p privateKeyPwd | -i] [-o signatureFn] [-kn nameOfAdditionalKey]
Create detached signature for file. Can be combined to container using "ubcli crypto combine"
- if "-kn nameOfAdditionalKey" specified - use additional key (one of dstu.iit.additionalKeys) to sign
- if "-k privateKeyPath" is not specified - use key from "ubConfig.security.dstu.iit.keyPath"
- if "-p privateKeyPwd" is not specified - use password from "ubConfig.security.dstu.iit.password",
- "-i" is specified - ask for password form stdin
- "signFormat" can be one of "CAdES", "XAdES"
- "-o signatureFn" is a result file name, if not specified - output result to fileName4Sign folder in fileName4Sign.[p7s|xml]
signAsic fileNames4Sign signFormat [-k privateKeyPath] [-p privateKeyPwd | -i] [-o signatureFn] [-kn nameOfAdditionalKey]
Create ASiC-E-signFormat (ASiC-E-CAdES or ASiC-E-XAdES) container for file(s) (; separated)
- if "-kn nameOfAdditionalKey" specified - use additional key (one of dstu.iit.additionalKeys) to sign
- if "-k privateKeyPath" is not specified - use key from "ubConfig.security.dstu.iit.keyPath"
- if "-p privateKeyPwd" is not specified - use password from "ubConfig.security.dstu.iit.password",
- "-i" is specified - ask for password form stdin
- "signFormat" can be one of "CAdES", "XAdES"
- "-o signatureFn" is a result file name, if not specified - output result to first fileName4Sign folder in firstFileName4Sign.asice
verify signFn [dataFn]
Verify signature. If "dataFn" is not specified consider "signFn" is container with signature(s) and data
hash fileName [algorithm] [-b] [-cert /part/to/signing.cer]
Calculate hash of file. Default algorithm is GOST (GOST-34311).
- possible algorithm values are "GOST","MD5","SHA1","SHA256","SHA384","SHA512","SHA3_256","SHA3_512"
- default output id base64 encoded hash value; if "-b" specified - hexadecimal
- if '-cert /part/to/signing.cer' is passed - hashing parameters will be obtained from certificate
split containerFn [destFolder]
Split container into signatures and optional data
combine dataFn signatureFn [-ss secondarySignatureFn] [-forceAsicS] [-d destFolder]
Combine data with signature (and optionally 2'd signature)
- type of result container is auto-detected, but can be forced to create ASiC-S (if possible)
- second signature can be specified in -ss
- if destination is not specified - will write result to current folder
enumDev Enumerate available devices for private key operations
certParse pathToCertOrFolder
Parse certificate (or all certificates if path is folder) and output parsed JSON into stdout
`)
}
/**
* Display JSON with available key medias and devices
*/
function enumDev () {
console.log('Available key medias: ')
console.log(JSON.stringify(iitCrypto.getAllKeyMedia(), null, ' '))
}
/**
* Create detached signature for file. [-k privateKeyPath] [-p privateKeyPwd | -i] [-o signatureFn] [-kn additionalKeyName]
* - if "-k privateKeyPath" is not specified - use key from "ubConfig.security.dstu.iit.keyPath"
* - if "-p privateKeyPwd" is not specified - use password from "ubConfig.security.dstu.iit.password",
* - "-i" is specified - ask for password form stdin
* - "signFormat" can be one of "CAdES", "XAdES"
* - "-o signatureFn" is a result file name, if not specified - output result to stdin
*
* @param {string} fileName4Sign
* @param {string} signFormat one of "CAdES", "XAdES"
*/
function sign (fileName4Sign, signFormat) {
if (!signFormat || signFormat.startsWith('-')) signFormat = 'CAdES'
const keyName = options.switchValue('kn') || ''
if (keyName) {
const additionalKeysCfg = iitSettings.additionalKeys || []
iitCrypto.initAdditionalKeys(additionalKeysCfg) // for cmd line init additional keys manually
} else {
let keyPath = options.switchValue('k')
if (!keyPath) keyPath = iitSettings && iitSettings.keyPath
if (!keyPath) throw new Error("Private key is not specified, either use '-kn KeyName' to specify additional key name fro config, or '-k keyPath' or define 'dstu.iit.keyPath' in ubConfig")
let keyPwd
if (options.switchIndex('i') !== -1) {
console.write('Private key password:')
keyPwd = console.readLn()
} else {
keyPwd = options.switchValue('p')
if (!keyPwd) keyPwd = iitSettings && iitSettings.password
if (!keyPwd) throw new Error('Private key password is not specified as -p parameter and not found in ubConfig. Use -i to ask for password from stdin')
}
iitCrypto.readPkFromFile(keyPath, keyPwd)
}
const fullFn = buildFullPath(fileName4Sign)
let res
if (signFormat === 'CAdES') {
res = iitCrypto.sign(fullFn, { keyName })
} else if (signFormat === 'XAdES') {
throw new Error('not implemented XAdES sign')
} else {
throw new Error('Invalid sign format. Possible values are CAdES or XAdES')
}
let outFn = options.switchValue('o')
if (outFn) {
outFn = buildFullPath(outFn)
} else {
outFn = fullFn + iitCrypto.CONST.FILE_EXT[signFormat]
}
fs.writeFileSync(outFn, res)
console.log('Signature is written to: ', outFn)
}
/**
* Create ASiC-E-signFormat (ASiC-E-CAdES or ASiC-E-XAdES) container for file(s) (; separated).
* - if "-k privateKeyPath" is not specified - use key from "ubConfig.security.dstu.iit.keyPath"
* - if "-p privateKeyPwd" is not specified - use password from "ubConfig.security.dstu.iit.password",
* - "-i" is specified - ask for password form stdin
* - "signFormat" can be one of "CAdES", "XAdES"
* - "-o signatureFn" is a result file name, if not specified - output result to first fileName4Sign folder in firstFileName4Sign.asice
*
* @param {string} fileName4Sign
* @param {string} signFormat one of "CAdES", "XAdES"
*/
function signAsic (fileName4Sign, signFormat) {
if (!signFormat || signFormat.startsWith('-')) signFormat = 'CAdES'
const keyName = options.switchValue('kn') || ''
if (keyName) {
const additionalKeysCfg = iitSettings.additionalKeys || []
iitCrypto.initAdditionalKeys(additionalKeysCfg) // for cmd line init additional keys manually
} else {
let keyPath = options.switchValue('k')
if (!keyPath) keyPath = iitSettings && iitSettings.keyPath
if (!keyPath) throw new Error('Private key path is not specified as -k parameter and not found in ubConfig')
let keyPwd
if (options.switchIndex('i') !== -1) {
console.write('Private key password:')
keyPwd = console.readLn()
} else {
keyPwd = options.switchValue('p')
if (!keyPwd) keyPwd = iitSettings && iitSettings.password
if (!keyPwd) throw new Error('Private key password is not specified as -p parameter and not found in ubConfig. Use -i to ask for password from stdin')
}
iitCrypto.readPkFromFile(keyPath, keyPwd)
}
const files = fileName4Sign.split(';').map(fn => {
return {
dataOrDataPath: buildFullPath(fn)
}
})
let outFn = options.switchValue('o')
if (outFn) {
outFn = buildFullPath(outFn)
} else {
outFn = files[0].dataOrDataPath + iitCrypto.CONST.FILE_EXT.ASiCE
}
const res = iitCrypto.signAsASiC(files, outFn, { keyName }) // result is output file name
console.log('Signature is written to: ', res)
}
/**
* Split any container (with one data file) into data file and signatures
* @param {string} containerFn Container file name
* @param {string }destFolder Destination folder for extraction
*/
function split (containerFn, destFolder) {
if (!containerFn || !destFolder) {
console.error('usage: ubcli split containerFn destFolder')
return
}
const buf = fileAsBuf(containerFn)
const res = iitCrypto.splitAnyContainer(buf)
if (!res) {
console.error("Can't split")
return
}
// write data file
const signsCnt = res.signatures.length
console.info(`Container '${containerFn}' is ${res.containerType} with ${signsCnt} '${res.containerSignFormat}' signature ${signsCnt > 1 ? '(s)' : ''}`)
const fullDest = ensueFolder(destFolder)
// write signature(s)
const dataFN = path.join(fullDest, res.dataFileName || 'data.bin')
fs.writeFileSync(dataFN, res.data)
let ext = iitCrypto.CONST.FILE_EXT[res.containerSignFormat]
if ((res.containerType === iitCrypto.CONST.C_TYPES.ASiCE) && (res.containerSignFormat === iitCrypto.CONST.SIGN_FORMAT.CAdES)) {
console.warn('Signature from ASiC-E CAdES extracted as UB specific zip file without data - this is not a signature from IIT POV')
ext = '.zip'
}
res.signatures.forEach((s, i) => {
const sNum = (i + 1).toString().padStart(3, '0')
const fn = path.join(fullDest, `signature${sNum}${ext}`)
fs.writeFileSync(fn, s)
})
console.log(`Data and signatures are written to ${fullDest}`)
}
/**
* Combine data and signature(s) into one container
* @param {string} dataFn
* @param {string} signatureFn
*/
function combine (dataFn, signatureFn) {
const signatures = []
signatures.push(fileAsBuf(signatureFn))
const secFn = options.switchValue('ss')
if (secFn) {
signatures.push(fileAsBuf(secFn))
}
const forceASiCS = options.switchIndex('forceAsicS') !== -1
const destFolder = options.switchValue('d') || './'
const { container, preferredResultName, containerType, containerSignFormat } = iitCrypto.combineAnyContainer({
dataOrDataPath: buildFullPath(dataFn),
signatures,
forceASiCS
})
const fullDest = ensueFolder(destFolder)
const fn = path.join(fullDest, preferredResultName)
fs.writeFileSync(fn, container)
console.log(`Container of type ${containerType} with ${containerSignFormat} signature(s) is written to ${fn}`)
}
/**
*
* @param {string} dataFn
* @param {string} [algo]
*/
function hash (dataFn, algo) {
if (!algo || algo.startsWith('-')) algo = 'GOST'
const fullFn = buildFullPath(dataFn)
const hexa = options.switchIndex('b') !== -1
let res
if (algo === 'GOST') {
let hashBin
const certPath = options.switchValue('cert')
if (certPath) {
const fullCertPath = buildFullPath(certPath)
const certDataBin = fs.readFileSync(fullCertPath)
hashBin = iitCrypto.ctxHash(fullFn, certDataBin)
} else {
hashBin = iitCrypto.ctxHash(fullFn)
}
res = Buffer.from(hashBin).toString(hexa ? 'hex' : 'base64')
} else if (['MD5', 'SHA1', 'SHA256', 'SHA384', 'SHA512', 'SHA3_256', 'SHA3_512'].includes(algo)) {
// eslint-disable-next-line no-undef
const hashHexa = nhashFile(fullFn, algo)
res = hexa ? hashHexa : Buffer.from(hashHexa, 'hex').toString('base64')
} else {
throw new Error(`Invalid hash algorithm ${algo}`)
}
console.log(res)
}
/**
* Verify signature
* @param {string} signFn
* @param {string} [dataFn]
*/
function verify (signFn, dataFn) {
const fullSignFn = buildFullPath(signFn)
const fullDataFn = dataFn ? buildFullPath(dataFn) : null
const signBuf = fs.readFileSync(fullSignFn, { encoding: 'bin' })
const res = iitCrypto.verify(signBuf, fullDataFn)
console.log(JSON.stringify(res, null, ' '))
}
/**
* Parse certificate(s)
* @param {string} certOrFolderPath
*/
function certParse (certOrFolderPath) {
const certOrFolderFullPath = buildFullPath(certOrFolderPath)
const stat = fs.statSync(certOrFolderFullPath)
if (!stat.isFile()) { // enum all *.cer in folder
const files = fs.readdirSync(certOrFolderFullPath)
files.forEach(f => {
// if (f.endsWith('.cer'))
console.log('\n=====', f, '=====')
const certFn = path.join(certOrFolderFullPath, f)
try {
const certBuf = fs.readFileSync(certFn, { encoding: 'bin' })
console.log(JSON.stringify(iitCrypto.parseCertificate(certBuf), null, ' '))
} catch (e) {
console.error(`File: ${f}, Error: ${e.message}`)
}
})
} else {
const certBuf = fs.readFileSync(certOrFolderFullPath, { encoding: 'bin' })
console.log(JSON.stringify(iitCrypto.parseCertificate(certBuf), null, ' '))
}
}
/**
* For relative path transform it to absolute relative to cwd
* @param {string} fn
* @returns {string}
*/
function buildFullPath (fn) {
return path.isAbsolute(fn)
? fn
: path.join(process.cwd(), fn)
}
/**
* return file as buffer or throw
* @private
* @param {string} fn
* @returns {Buffer}
*/
function fileAsBuf (fn) {
const fullFn = buildFullPath(fn)
if (!fs.existsSync(fullFn)) {
throw new Error(`Specified file '${fullFn}' dose not exists`)
}
return fs.readFileSync(fullFn, { encoding: 'bin' })
}
/**
* Create absolute folder path and force to create it
* @param {string} folderPath
* @returns {string} absolute folder path
*/
function ensueFolder (folderPath) {
const fullDest = path.isAbsolute(folderPath)
? folderPath
: path.join(process.cwd(), folderPath)
if (!fs.existsSync(fullDest)) {
console.log(`Creating ${fullDest} folder`)
fs.mkdirSync(fullDest)
}
return fullDest
}