Skip to content

Commit 08cd1ed

Browse files
committed
Use binary buffers for hash; safe64 encode results. Addresses compiler-explorer#1056
1 parent 2d23acc commit 08cd1ed

File tree

3 files changed

+76
-8
lines changed

3 files changed

+76
-8
lines changed

Diff for: lib/storage/storage.js

+13-3
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
// POSSIBILITY OF SUCH DAMAGE.
2525

2626
const logger = require('../logger').logger,
27-
hash = require('../utils').getHash;
27+
hash = require('../utils').getBinaryHash;
2828

2929
const FILE_HASH_VERSION = 'Compiler Explorer Config Hasher';
3030

@@ -34,12 +34,22 @@ class StorageBase {
3434
this.compilerProps = compilerProps;
3535
}
3636

37+
/**
38+
* Encode a buffer as a URL-safe string.
39+
* @param {Buffer} buffer
40+
* @returns {string}
41+
*/
42+
static safe64Encoded(buffer) {
43+
return buffer.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
44+
}
45+
3746
handler(req, res) {
3847
const config = JSON.stringify(req.body.config);
39-
const configHash = Buffer.from(hash(config, FILE_HASH_VERSION)).toString('base64');
48+
const configHash = StorageBase.safe64Encoded(hash(config, FILE_HASH_VERSION));
4049
this.findUniqueSubhash(configHash)
4150
.then(result => {
42-
logger.info(`Unique subhash ${result.uniqueSubHash} (${result.alreadyPresent})`);
51+
logger.info(`Unique subhash '${result.uniqueSubHash}' ` +
52+
`(${result.alreadyPresent ? "was already present" : "newly-created"})`);
4353
if (!result.alreadyPresent) {
4454
const storedObject = {
4555
prefix: result.prefix,

Diff for: lib/utils.js

+24-5
Original file line numberDiff line numberDiff line change
@@ -108,19 +108,38 @@ exports.anonymizeIp = function anonymizeIp(ip) {
108108
}
109109
};
110110

111+
function objectToHashableString(object) {
112+
// See https://stackoverflow.com/questions/899574/which-is-best-to-use-typeof-or-instanceof/6625960#6625960
113+
return (typeof(object) === 'string') ? object : JSON.stringify(object);
114+
}
115+
116+
const DefaultHash = 'Compiler Explorer Default Version 1';
117+
118+
/***
119+
* Gets the hash (as a binary buffer) of the given object
120+
*
121+
* Limitation: object shall not have circular references
122+
* @param object {*} Object to get hash from
123+
* @param HashVersion {String} Hash "version"
124+
* @returns {Buffer} Hash of object
125+
*/
126+
exports.getBinaryHash = function getHash(object, HashVersion = DefaultHash) {
127+
return crypto.createHmac('sha256', HashVersion)
128+
.update(objectToHashableString(object))
129+
.digest('buffer');
130+
};
131+
111132
/***
112-
* Gets the hash of the given object
133+
* Gets the hash (as a hex string) of the given object
113134
*
114135
* Limitation: object shall not have circular references
115136
* @param object {*} Object to get hash from
116137
* @param HashVersion {String} Hash "version"
117138
* @returns {String} Hash of object
118139
*/
119-
exports.getHash = function getHash(object, HashVersion = 'Compiler Explorer Default Version 1') {
120-
// See https://stackoverflow.com/questions/899574/which-is-best-to-use-typeof-or-instanceof/6625960#6625960
121-
const asString = (typeof(object) === 'string') ? object : JSON.stringify(object);
140+
exports.getHash = function getHash(object, HashVersion = DefaultHash) {
122141
return crypto.createHmac('sha256', HashVersion)
123-
.update(asString)
142+
.update(objectToHashableString(object))
124143
.digest('hex');
125144
};
126145

Diff for: test/storage/storage-tests.js

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright (c) 2018, Compiler Explorer Authors
2+
// All rights reserved.
3+
//
4+
// Redistribution and use in source and binary forms, with or without
5+
// modification, are permitted provided that the following conditions are met:
6+
//
7+
// * Redistributions of source code must retain the above copyright notice,
8+
// this list of conditions and the following disclaimer.
9+
// * Redistributions in binary form must reproduce the above copyright
10+
// notice, this list of conditions and the following disclaimer in the
11+
// documentation and/or other materials provided with the distribution.
12+
//
13+
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
14+
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15+
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
16+
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
17+
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18+
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19+
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20+
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21+
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22+
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
23+
// POSSIBILITY OF SUCH DAMAGE.
24+
25+
const chai = require('chai'),
26+
{StorageBase} = require('../../lib/storage/storage');
27+
28+
chai.should();
29+
30+
describe('Hash tests', () => {
31+
it('should never generate invalid characters', () => {
32+
for (let i = 0; i < 256; ++i) {
33+
const buf = new Buffer([i]);
34+
const as64 = StorageBase.safe64Encoded(buf);
35+
as64.should.not.contain("/");
36+
as64.should.not.contain("+");
37+
}
38+
});
39+
});

0 commit comments

Comments
 (0)