JS: Modernize SignedSource
Reviewed By: cpojer Differential Revision: D4271506 fbshipit-source-id: b3eff94de9ce2c179d62d57eace9a40797aed397
This commit is contained in:
parent
dd97443f2b
commit
ebe0c85fbe
|
@ -8,93 +8,110 @@
|
||||||
*/
|
*/
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var TOKEN = '<<SignedSource::*O*zOeWoEQle#+L!plEphiEmie@IsG>>',
|
const crypto = require('crypto');
|
||||||
OLDTOKEN = '<<SignedSource::*O*zOeWoEQle#+L!plEphiEmie@I>>',
|
|
||||||
TOKENS = [TOKEN, OLDTOKEN],
|
|
||||||
PATTERN = new RegExp('@' + 'generated (?:SignedSource<<([a-f0-9]{32})>>)');
|
|
||||||
|
|
||||||
exports.SIGN_OK = {message:'ok'};
|
const GENERATED = '@' + 'generated';
|
||||||
exports.SIGN_UNSIGNED = new Error('unsigned');
|
const OLDTOKEN = '<<SignedSource::*O*zOeWoEQle#+L!plEphiEmie@I>>';
|
||||||
exports.SIGN_INVALID = new Error('invalid');
|
const NEWTOKEN = '<<SignedSource::*O*zOeWoEQle#+L!plEphiEmie@IsG>>';
|
||||||
|
const TOKENS = [NEWTOKEN, OLDTOKEN];
|
||||||
|
const PATTERN = new RegExp(`${GENERATED} (?:SignedSource<<([a-f0-9]{32})>>)`);
|
||||||
|
|
||||||
// Thrown by sign(). Primarily for unit tests.
|
const TokenNotFoundError = new Error(
|
||||||
exports.TokenNotFoundError = new Error(
|
`SignedSource.signFile(...): Cannot sign file without token: ${NEWTOKEN}`
|
||||||
'Code signing placeholder not found (expected to find \'' + TOKEN + '\')');
|
);
|
||||||
|
|
||||||
var md5_hash_hex;
|
function hash(data, encoding) {
|
||||||
|
const md5sum = crypto.createHash('md5');
|
||||||
// MD5 hash function for Node.js. To port this to other platforms, provide an
|
md5sum.update(data, encoding);
|
||||||
// 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);
|
|
||||||
return md5sum.digest('hex');
|
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,
|
// @deprecated
|
||||||
// in the file you wish to be signed.
|
SignedSource.SIGN_OK = {message: 'ok'};
|
||||||
//
|
SignedSource.SIGN_INVALID = new Error('invalid');
|
||||||
// @return str to be embedded in to-be-signed file
|
SignedSource.SIGN_UNSIGNED = new Error('unsigned');
|
||||||
function signing_token() {
|
SignedSource.signing_token = SignedSource.getSigningToken;
|
||||||
return '@' + 'generated ' + TOKEN;
|
SignedSource.is_signed = SignedSource.isSigned;
|
||||||
}
|
SignedSource.sign = data => ({
|
||||||
exports.signing_token = signing_token;
|
first_time: data.includes(NEWTOKEN),
|
||||||
|
signed_data: SignedSource.signFile(data),
|
||||||
// Determine whether a file is signed. This does NOT verify the signature.
|
});
|
||||||
//
|
SignedSource.verify_signature = data => {
|
||||||
// @param str File contents as a string.
|
try {
|
||||||
// @return bool True if the file has a signature.
|
return SignedSource.verifySignature(data)
|
||||||
function is_signed(file_data) {
|
? SignedSource.SIGN_OK
|
||||||
return !!PATTERN.exec(file_data);
|
: SignedSource.SIGN_INVALID;
|
||||||
}
|
} catch (_) {
|
||||||
exports.is_signed = is_signed;
|
return SignedSource.SIGN_UNSIGNED;
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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.
|
module.exports = SignedSource;
|
||||||
//
|
|
||||||
// @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;
|
|
||||||
|
|
Loading…
Reference in New Issue