Skip to content

Commit 3aba44c

Browse files
committed
re-add hmac+sha256 library (#364)
1 parent 63964fd commit 3aba44c

File tree

4 files changed

+293
-5
lines changed

4 files changed

+293
-5
lines changed

html5/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
<script type="text/javascript" src="js/lib/rencode.js"></script>
2727
<script type="text/javascript" src="js/lib/lz4.js"></script>
2828
<script type="text/javascript" src="js/lib/brotli_decode.js"></script>
29+
<script type="text/javascript" src="js/lib/hmac.js"></script>
2930

3031
<script type="text/javascript" src="js/lib/jsmpeg.js"></script>
3132
<script type="text/javascript" src="js/lib/aurora/aurora.js"></script>

html5/js/Client.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1454,7 +1454,7 @@ class XpraClient {
14541454
}
14551455

14561456
_get_digests() {
1457-
const digests = ["xor", "keycloak"];
1457+
const digests = ["xor", "keycloak", "hmac+sha256"];
14581458

14591459
if (typeof crypto.subtle !== "undefined") {
14601460
try {

html5/js/Utilities.js

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,8 @@ const Utilities = {
101101
},
102102

103103
gendigest(digest, password, salt) {
104-
Utilities.clog("gendigest(", digest, ", ", password, ", ", salt, ")");
104+
// Utilities.clog("gendigest(", digest, ", ", password, ", ", salt, ")");
105+
// Utilities.clog("gendigest(", digest, ", ", Utilities.convertToHex(password), ", ", Utilities.convertToHex(salt), ")");
105106
if (digest == "xor") {
106107
const trimmed_salt = salt.slice(0, password.length);
107108
// Utilities.debug("xoring with trimmed salt:", Utilities.convertToHex(trimmed_salt));
@@ -126,6 +127,19 @@ const Utilities = {
126127
hash = "SHA-" + hash.substring(3);
127128
}
128129

130+
if (typeof crypto.subtle === "undefined") {
131+
// use hmac.js
132+
return new Promise(function(resolve, reject) {
133+
if (hash != "SHA-256") {
134+
reject(new Error("crypto.subtle API is not available in this context"));
135+
}
136+
else {
137+
value = hmac(Utilities.u8(password), Utilities.u8(salt));
138+
resolve(value);
139+
}
140+
});
141+
}
142+
129143
const promise = new Promise(function(resolve, reject) {
130144
Utilities.clog("crypto.subtle=", crypto.subtle);
131145
Utilities.clog("crypto.subtle.importKey=", crypto.subtle.importKey);
@@ -167,10 +181,13 @@ const Utilities = {
167181
string_;
168182
},
169183

170-
convertToHex(string_) {
184+
convertToHex(value) {
185+
if (value instanceof Uint8Array) {
186+
return Utilities.arrayhex(value);
187+
}
171188
let hex = "";
172-
for (let index = 0; index < string_.length; index++) {
173-
hex += `${string_.charCodeAt(index).toString(16).padStart(2, "0")}`;
189+
for (let index = 0; index < value.length; index++) {
190+
hex += `${value.charCodeAt(index).toString(16).padStart(2, "0")}`;
174191
}
175192
return hex;
176193
},

html5/js/lib/hmac.js

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
// https://gist.github.com/stevendesu/2d52f7b5e1f1184af3b667c0b5e054b8?permalink_comment_id=4535312
2+
// A simple, open-source, HMAC-SHA256 implementation in pure JavaScript. Designed for efficient minification.
3+
// Feel free to choose whatever license you find most permissible, but I offer no warranty for the code.
4+
// It's 100% free to do with as you please.
5+
6+
// To ensure cross-browser support even without a proper SubtleCrypto
7+
// impelmentation (or without access to the impelmentation, as is the case with
8+
// Chrome loaded over HTTP instead of HTTPS), this library can create SHA-256
9+
// HMAC signatures using nothing but raw JavaScript
10+
11+
/* eslint-disable no-magic-numbers, id-length, no-param-reassign, new-cap */
12+
13+
// By giving internal functions names that we can mangle, future calls to
14+
// them are reduced to a single byte (minor space savings in minified file)
15+
var uint8Array = Uint8Array;
16+
var uint32Array = Uint32Array;
17+
var pow = Math.pow;
18+
19+
// Will be initialized below
20+
// Using a Uint32Array instead of a simple array makes the minified code
21+
// a bit bigger (we lose our `unshift()` hack), but comes with huge
22+
// performance gains
23+
var DEFAULT_STATE = new uint32Array(8);
24+
var ROUND_CONSTANTS = [];
25+
26+
// Reusable object for expanded message
27+
// Using a Uint32Array instead of a simple array makes the minified code
28+
// 7 bytes larger, but comes with huge performance gains
29+
var M = new uint32Array(64);
30+
31+
// After minification the code to compute the default state and round
32+
// constants is smaller than the output. More importantly, this serves as a
33+
// good educational aide for anyone wondering where the magic numbers come
34+
// from. No magic numbers FTW!
35+
function getFractionalBits(n)
36+
{
37+
return ((n - (n | 0)) * pow(2, 32)) | 0;
38+
}
39+
40+
var n = 2, nPrime = 0;
41+
while (nPrime < 64)
42+
{
43+
// isPrime() was in-lined from its original function form to save
44+
// a few bytes
45+
var isPrime = true;
46+
// Math.sqrt() was replaced with pow(n, 1/2) to save a few bytes
47+
// var sqrtN = pow(n, 1 / 2);
48+
// So technically to determine if a number is prime you only need to
49+
// check numbers up to the square root. However this function only runs
50+
// once and we're only computing the first 64 primes (up to 311), so on
51+
// any modern CPU this whole function runs in a couple milliseconds.
52+
// By going to n / 2 instead of sqrt(n) we net 8 byte savings and no
53+
// scaling performance cost
54+
for (var factor = 2; factor <= n / 2; factor++)
55+
{
56+
if (n % factor === 0)
57+
{
58+
isPrime = false;
59+
}
60+
}
61+
if (isPrime)
62+
{
63+
if (nPrime < 8)
64+
{
65+
DEFAULT_STATE[nPrime] = getFractionalBits(pow(n, 1 / 2));
66+
}
67+
ROUND_CONSTANTS[nPrime] = getFractionalBits(pow(n, 1 / 3));
68+
69+
nPrime++;
70+
}
71+
72+
n++;
73+
}
74+
75+
// For cross-platform support we need to ensure that all 32-bit words are
76+
// in the same endianness. A UTF-8 TextEncoder will return BigEndian data,
77+
// so upon reading or writing to our ArrayBuffer we'll only swap the bytes
78+
// if our system is LittleEndian (which is about 99% of CPUs)
79+
var LittleEndian = !!new uint8Array(new uint32Array([1]).buffer)[0];
80+
81+
function convertEndian(word)
82+
{
83+
if (LittleEndian)
84+
{
85+
return (
86+
// byte 1 -> byte 4
87+
(word >>> 24) |
88+
// byte 2 -> byte 3
89+
(((word >>> 16) & 0xff) << 8) |
90+
// byte 3 -> byte 2
91+
((word & 0xff00) << 8) |
92+
// byte 4 -> byte 1
93+
(word << 24)
94+
);
95+
}
96+
else
97+
{
98+
return word;
99+
}
100+
}
101+
102+
function rightRotate(word, bits)
103+
{
104+
return (word >>> bits) | (word << (32 - bits));
105+
}
106+
107+
function sha256(data)
108+
{
109+
// Copy default state
110+
var STATE = DEFAULT_STATE.slice();
111+
112+
// Caching this reduces occurrences of ".length" in minified JavaScript
113+
// 3 more byte savings! :D
114+
var legth = data.length;
115+
116+
// Pad data
117+
var bitLength = legth * 8;
118+
var newBitLength = (512 - ((bitLength + 64) % 512) - 1) + bitLength + 65;
119+
120+
// "bytes" and "words" are stored BigEndian
121+
var bytes = new uint8Array(newBitLength / 8);
122+
var words = new uint32Array(bytes.buffer);
123+
124+
bytes.set(data, 0);
125+
// Append a 1
126+
bytes[legth] = 0b10000000;
127+
// Store length in BigEndian
128+
words[words.length - 1] = convertEndian(bitLength);
129+
130+
// Loop iterator (avoid two instances of "var") -- saves 2 bytes
131+
var round;
132+
133+
// Process blocks (512 bits / 64 bytes / 16 words at a time)
134+
for (var block = 0; block < newBitLength / 32; block += 16)
135+
{
136+
var workingState = STATE.slice();
137+
138+
// Rounds
139+
for (round = 0; round < 64; round++)
140+
{
141+
var MRound;
142+
// Expand message
143+
if (round < 16)
144+
{
145+
// Convert to platform Endianness for later math
146+
MRound = convertEndian(words[block + round]);
147+
}
148+
else
149+
{
150+
var gamma0x = M[round - 15];
151+
var gamma1x = M[round - 2];
152+
MRound =
153+
M[round - 7] + M[round - 16] + (
154+
rightRotate(gamma0x, 7) ^
155+
rightRotate(gamma0x, 18) ^
156+
(gamma0x >>> 3)
157+
) + (
158+
rightRotate(gamma1x, 17) ^
159+
rightRotate(gamma1x, 19) ^
160+
(gamma1x >>> 10)
161+
)
162+
;
163+
}
164+
165+
// M array matches platform endianness
166+
M[round] = MRound |= 0;
167+
168+
// Computation
169+
var t1 =
170+
(
171+
rightRotate(workingState[4], 6) ^
172+
rightRotate(workingState[4], 11) ^
173+
rightRotate(workingState[4], 25)
174+
) +
175+
(
176+
(workingState[4] & workingState[5]) ^
177+
(~workingState[4] & workingState[6])
178+
) + workingState[7] + MRound + ROUND_CONSTANTS[round]
179+
;
180+
var t2 =
181+
(
182+
rightRotate(workingState[0], 2) ^
183+
rightRotate(workingState[0], 13) ^
184+
rightRotate(workingState[0], 22)
185+
) +
186+
(
187+
(workingState[0] & workingState[1]) ^
188+
(workingState[2] & (workingState[0] ^
189+
workingState[1]))
190+
)
191+
;
192+
193+
for (var i = 7; i > 0; i--)
194+
{
195+
workingState[i] = workingState[i - 1];
196+
}
197+
workingState[0] = (t1 + t2) | 0;
198+
workingState[4] = (workingState[4] + t1) | 0;
199+
}
200+
201+
// Update state
202+
for (round = 0; round < 8; round++)
203+
{
204+
STATE[round] = (STATE[round] + workingState[round]) | 0;
205+
}
206+
}
207+
208+
// Finally the state needs to be converted to BigEndian for output
209+
// And we want to return a Uint8Array, not a Uint32Array
210+
return new uint8Array(new uint32Array(
211+
STATE.map(function(val) { return convertEndian(val); })
212+
).buffer);
213+
}
214+
215+
function hmac(key, data)
216+
{
217+
if (key.length > 64)
218+
key = sha256(key);
219+
220+
if (key.length < 64)
221+
{
222+
const tmp = new Uint8Array(64);
223+
tmp.set(key, 0);
224+
key = tmp;
225+
}
226+
227+
// Generate inner and outer keys
228+
var innerKey = new Uint8Array(64);
229+
var outerKey = new Uint8Array(64);
230+
for (var i = 0; i < 64; i++)
231+
{
232+
innerKey[i] = 0x36 ^ key[i];
233+
outerKey[i] = 0x5c ^ key[i];
234+
}
235+
236+
// Append the innerKey
237+
var msg = new Uint8Array(data.length + 64);
238+
msg.set(innerKey, 0);
239+
msg.set(data, 64);
240+
241+
// Has the previous message and append the outerKey
242+
var result = new Uint8Array(64 + 32);
243+
result.set(outerKey, 0);
244+
result.set(sha256(msg), 64);
245+
246+
// Hash the previous message
247+
return sha256(result);
248+
}
249+
250+
// Convert a string to a Uint8Array, SHA-256 it, and convert back to string
251+
const encoder = new TextEncoder("utf-8");
252+
253+
function sign(inputKey, inputData)
254+
{
255+
const key = typeof inputKey === "string" ? encoder.encode(inputKey) : inputKey;
256+
const data = typeof inputData === "string" ? encoder.encode(inputData) : inputData;
257+
return hmac(key, data);
258+
}
259+
260+
function hash(str)
261+
{
262+
return hex(sha256(encoder.encode(str)));
263+
}
264+
265+
function hex(bin)
266+
{
267+
return bin.reduce((acc, val) =>
268+
acc + ("00" + val.toString(16)).substr(-2)
269+
, "");
270+
}

0 commit comments

Comments
 (0)