diff --git a/local-cli/bundle/signedsource.js b/local-cli/bundle/signedsource.js index 2876a53be..94b51ce9e 100644 --- a/local-cli/bundle/signedsource.js +++ b/local-cli/bundle/signedsource.js @@ -8,93 +8,110 @@ */ 'use strict'; -var TOKEN = '<>', - OLDTOKEN = '<>', - TOKENS = [TOKEN, OLDTOKEN], - PATTERN = new RegExp('@' + 'generated (?:SignedSource<<([a-f0-9]{32})>>)'); +const crypto = require('crypto'); -exports.SIGN_OK = {message:'ok'}; -exports.SIGN_UNSIGNED = new Error('unsigned'); -exports.SIGN_INVALID = new Error('invalid'); +const GENERATED = '@' + 'generated'; +const OLDTOKEN = '<>'; +const NEWTOKEN = '<>'; +const TOKENS = [NEWTOKEN, OLDTOKEN]; +const PATTERN = new RegExp(`${GENERATED} (?:SignedSource<<([a-f0-9]{32})>>)`); -// Thrown by sign(). Primarily for unit tests. -exports.TokenNotFoundError = new Error( - 'Code signing placeholder not found (expected to find \'' + TOKEN + '\')'); +const TokenNotFoundError = new Error( + `SignedSource.signFile(...): Cannot sign file without token: ${NEWTOKEN}` +); -var md5_hash_hex; - -// MD5 hash function for Node.js. To port this to other platforms, provide an -// alternate code path for defining the md5_hash_hex function. -var crypto = require('crypto'); -// eslint-disable-next-line no-shadow -md5_hash_hex = function md5_hash_hex(data, input_encoding) { - var md5sum = crypto.createHash('md5'); - md5sum.update(data, input_encoding); +function hash(data, encoding) { + const md5sum = crypto.createHash('md5'); + md5sum.update(data, encoding); return md5sum.digest('hex'); +} + +/** + * Utility for signing and verifying the signature of a file. This is useful for + * ensuring that the contents of a generated file are not contaminated by manual + * changes. Example usage: + * + * const myFile = ` + * // ${SignedSource.getSigningToken()} + * + * console.log('My generated file.'); + * `; + * const mySignedFile = SignedSource.signFile(myFile); + * + */ +const SignedSource = { + TokenNotFoundError, + + /** + * Gets the signing token to be embedded in the file you wish to be signed. + */ + getSigningToken() { + return `${GENERATED} ${NEWTOKEN}`; + }, + + /** + * Checks whether a file is signed *without* verifying the signature. + */ + isSigned(data) { + return !PATTERN.exec(data); + }, + + /** + * Signs a source file which contains a signing token. Signing modifies only + * the signing token, so the token should be placed inside a comment in order + * for signing to not change code semantics. + */ + signFile(data) { + if (!data.includes(NEWTOKEN)) { + if (SignedSource.isSigned(data)) { + // Signing a file that was previously signed. + data = data.replace(PATTERN, SignedSource.getSigningToken()); + } else { + throw TokenNotFoundError; + } + } + return data.replace(NEWTOKEN, `SignedSource<<${hash(data, 'utf8')}>>`); + }, + + /** + * Verifies the signature in a signed file. + */ + verifySignature(data) { + const matches = PATTERN.exec(data); + if (!matches) { + throw new Error( + 'SignedSource.verifySignature(...): Cannot verify signature of an ' + + 'unsigned file.' + ); + } + const actual = matches[1]; + // Replace signature with `NEWTOKEN` and hash to see if it matches the hash + // in the file. For backwards compatibility, also try `OLDTOKEN`. + return TOKENS.some(token => { + const unsigned = data.replace(PATTERN, `${GENERATED} ${token}`); + return hash(unsigned, 'utf8') === actual; + }); + }, }; -// Returns the signing token to be embedded, generally in a header comment, -// in the file you wish to be signed. -// -// @return str to be embedded in to-be-signed file -function signing_token() { - return '@' + 'generated ' + TOKEN; -} -exports.signing_token = signing_token; - -// Determine whether a file is signed. This does NOT verify the signature. -// -// @param str File contents as a string. -// @return bool True if the file has a signature. -function is_signed(file_data) { - return !!PATTERN.exec(file_data); -} -exports.is_signed = is_signed; - -// Sign a source file which you have previously embedded a signing token -// into. Signing modifies only the signing token, so the semantics of the -// file will not change if you've put it in a comment. -// -// @param str File contents as a string (with embedded token). -// @return str Signed data. -function sign(file_data) { - var first_time = file_data.indexOf(TOKEN) !== -1; - if (!first_time) { - if (is_signed(file_data)) { - file_data = file_data.replace(PATTERN, signing_token()); - } else { - throw exports.TokenNotFoundError; - } +// @deprecated +SignedSource.SIGN_OK = {message: 'ok'}; +SignedSource.SIGN_INVALID = new Error('invalid'); +SignedSource.SIGN_UNSIGNED = new Error('unsigned'); +SignedSource.signing_token = SignedSource.getSigningToken; +SignedSource.is_signed = SignedSource.isSigned; +SignedSource.sign = data => ({ + first_time: data.includes(NEWTOKEN), + signed_data: SignedSource.signFile(data), +}); +SignedSource.verify_signature = data => { + try { + return SignedSource.verifySignature(data) + ? SignedSource.SIGN_OK + : SignedSource.SIGN_INVALID; + } catch (_) { + return SignedSource.SIGN_UNSIGNED; } - var signature = md5_hash_hex(file_data, 'utf8'); - var signed_data = file_data.replace(TOKEN, 'SignedSource<<' + signature + '>>'); - return { first_time: first_time, signed_data: signed_data }; -} -exports.sign = sign; +}; -// Verify a file's signature. -// -// @param str File contents as a string. -// @return Returns SIGN_OK if the data contains a valid signature, -// SIGN_UNSIGNED if it contains no signature, or SIGN_INVALID if -// it contains an invalid signature. -function verify_signature(file_data) { - var match = PATTERN.exec(file_data); - if (!match) { - return exports.SIGN_UNSIGNED; - } - // Replace the signature with the TOKEN, then hash and see if it matches - // the value in the file. For backwards compatibility, also try with - // OLDTOKEN if that doesn't match. - var k, token, with_token, actual_md5, expected_md5 = match[1]; - for (k in TOKENS) { - token = TOKENS[k]; - with_token = file_data.replace(PATTERN, '@' + 'generated ' + token); - actual_md5 = md5_hash_hex(with_token, 'utf8'); - if (expected_md5 === actual_md5) { - return exports.SIGN_OK; - } - } - return exports.SIGN_INVALID; -} -exports.verify_signature = verify_signature; +module.exports = SignedSource;