Project

General

Profile

« Previous | Next » 

Revision 2875397f

Added by koszko about 2 years ago

improve signing\n\nSignature timestamp is now handled in a saner way. Sha256 implementation is no longer pulled in contexts that don't require it.

View differences:

background/main.js
69 69
	    skip = true;
70 70
    }
71 71

  
72
    headers = inject_csp_headers(details, headers, policy);
72
    headers = inject_csp_headers(headers, policy);
73 73

  
74 74
    skip = skip || (details.statusCode >= 300 && details.statusCode < 400);
75 75
    if (!skip) {
background/policy_injector.js
16 16
 * IMPORTS_END
17 17
 */
18 18

  
19
function inject_csp_headers(details, headers, policy)
19
function inject_csp_headers(headers, policy)
20 20
{
21
    const url = details.url;
22

  
23
    let orig_csp_headers;
21
    let csp_headers;
24 22
    let old_signature;
25 23
    let hachette_header;
26 24

  
27 25
    for (const header of headers.filter(h => h.name === "x-hachette")) {
28
	const match = /^([^%])(%.*)$/.exec(header.value);
26
	/* x-hachette header has format: <signature>_0_<data> */
27
	const match = /^([^_]+)_(0_.*)$/.exec(header.value);
29 28
	if (!match)
30 29
	    continue;
31 30

  
32
	const old_data = extract_signed(...match.splice(1, 2), [[0]]);
33
	if (!old_data || old_data.url !== url)
31
	const result = extract_signed(...match.slice(1, 3));
32
	if (result.fail)
34 33
	    continue;
35 34

  
35
	/* This should succeed - it's our self-produced valid JSON. */
36
	const old_data = JSON.parse(decodeURIComponent(result.data));
37

  
36 38
	/* Confirmed- it's the originals, smuggled in! */
37
	orig_csp_headers = old_data.csp_headers;
39
	csp_headers = old_data.csp_headers;
38 40
	old_signature = old_data.policy_sig;
39 41

  
40 42
	hachette_header = header;
......
46 48
	headers.push(hachette_header);
47 49
    }
48 50

  
49
    orig_csp_headers = orig_csp_headers ||
51
    csp_headers = csp_headers ||
50 52
	headers.filter(h => is_csp_header_name(h.name));
51 53

  
52 54
    /* When blocking remove report-only CSP headers that snitch on us. */
53 55
    headers = headers.filter(h => !is_csp_header_name(h.name, !policy.allow));
54 56

  
55 57
    if (old_signature)
56
	headers = headers.filter(h => h.name.search(old_signature) === -1);
58
	headers = headers.filter(h => h.value.search(old_signature) === -1);
57 59

  
58
    const sanitizer = h => sanitize_csp_header(h, policy);
59
    headers.push(...orig_csp_headers.map(sanitizer));
60
    headers.push(...csp_headers.map(h => sanitize_csp_header(h, policy)));
60 61

  
61 62
    const policy_str = encodeURIComponent(JSON.stringify(policy));
62
    const policy_sig = sign_data(policy_str, new Date());
63
    const signed_policy = sign_data(policy_str, new Date().getTime());
63 64
    const later_30sec = new Date(new Date().getTime() + 30000).toGMTString();
64 65
    headers.push({
65 66
	name: "Set-Cookie",
66
	value: `hachette-${policy_sig}=${policy_str}; Expires=${later_30sec};`
67
	value: `hachette-${signed_policy.join("=")}; Expires=${later_30sec};`
67 68
    });
68 69

  
69 70
    /*
......
71 72
     * These are signed with a time of 0, as it's not clear there is a limit on
72 73
     * how long Firefox might retain headers in the cache.
73 74
     */
74
    let hachette_data = {csp_headers: orig_csp_headers, policy_sig, url};
75
    let hachette_data = {csp_headers, policy_sig: signed_policy[0]};
75 76
    hachette_data = encodeURIComponent(JSON.stringify(hachette_data));
76
    hachette_header.value = sign_data(hachette_data, 0) + hachette_data;
77
    hachette_header.value = sign_data(hachette_data, 0).join("_");
77 78

  
78 79
    /* To ensure there is a CSP header if required */
79 80
    if (!policy.allow)
common/misc.js
8 8

  
9 9
/*
10 10
 * IMPORTS_START
11
 * IMPORT sha256
12 11
 * IMPORT browser
13
 * IMPORT is_chrome
14 12
 * IMPORT TYPE_NAME
15 13
 * IMPORT TYPE_PREFIX
16 14
 * IMPORTS_END
......
45 43
    return Uint8toHex(randomData);
46 44
}
47 45

  
48
function get_secure_salt()
49
{
50
    if (is_chrome)
51
	return browser.runtime.getManifest().key.substring(0, 50);
52
    else
53
	return browser.runtime.getURL("dummy");
54
}
55

  
56
function extract_signed(signature, data, times)
57
{
58
    const now = new Date();
59
    times = times || [[now], [now, -1]];
60

  
61
    const reductor =
62
	  (ok, time) => ok || signature === sign_data(data, ...time);
63
    if (!times.reduce(reductor, false))
64
	return undefined;
65

  
66
    try {
67
	return JSON.parse(decodeURIComponent(data));
68
    } catch (e) {
69
	/* This should not be reached - it's our self-produced valid JSON. */
70
	console.log("Unexpected internal error - invalid JSON smuggled!", e);
71
    }
72
}
73

  
74 46
/* csp rule that blocks all scripts except for those injected by us */
75 47
function csp_rule(nonce)
76 48
{
......
89 61

  
90 62
function is_csp_header_name(string, include_report_only)
91 63
{
92
    string = string && string.toLowerCase() || "";
64
    string = string && string.toLowerCase().trim() || "";
93 65

  
94 66
    return (include_report_only && string === report_only_header_name) ||
95 67
	csp_header_names.has(string);
......
118 90
    return !!/^(chrome(-extension)?|moz-extension):\/\/|^about:/i.exec(url);
119 91
}
120 92

  
121
/* Sign a given string for a given time */
122
function sign_data(data, now, hours_offset) {
123
    let time = Math.floor(now / 3600000) + (hours_offset || 0);
124
    return sha256(get_secure_salt() + time + data);
125
}
126

  
127 93
/* Parse a CSP header */
128 94
function parse_csp(csp) {
129 95
    let directive, directive_array;
......
193 159
/*
194 160
 * EXPORTS_START
195 161
 * EXPORT gen_nonce
196
 * EXPORT extract_signed
197
 * EXPORT sign_data
198 162
 * EXPORT csp_rule
199 163
 * EXPORT is_csp_header_name
200 164
 * EXPORT nice_name
common/signing.js
1
/**
2
 * part of Hachette
3
 * Functions related to "signing" of data, refactored to a separate file.
4
 *
5
 * Copyright (C) 2021 Wojtek Kosior
6
 * Redistribution terms are gathered in the `copyright' file.
7
 */
8

  
9
/*
10
 * IMPORTS_START
11
 * IMPORT sha256
12
 * IMPORT browser
13
 * IMPORT is_chrome
14
 * IMPORTS_END
15
 */
16

  
17
/*
18
 * In order to make certain data synchronously accessible in certain contexts,
19
 * hachette smuggles it in string form in places like cookies, URLs and headers.
20
 * When using the smuggled data, we first need to make sure it isn't spoofed.
21
 * For that, we use this pseudo-signing mechanism.
22
 *
23
 * Despite what name suggests, no assymetric cryptography is involved, as it
24
 * would bring no additional benefits and would incur bigger performance
25
 * overhead. Instead, we hash the string data together with some secret value
26
 * that is supposed to be known only by this browser instance. Resulting hash
27
 * sum plays the role of the signature. In the hash we also include current
28
 * time. This way, even if signed data leaks (which shouldn't happen in the
29
 * first place), an attacker won't be able to re-use it indefinitely.
30
 *
31
 * The secret shared between execution contexts has to be available
32
 * synchronously. Under Mozilla, this is the extension's per-session id. Under
33
 * Chromium, this is the key that resides in the manifest.
34
 *
35
 * An idea to (under Chromium) instead store the secret in a file fetched
36
 * synchronously using XMLHttpRequest is being considered.
37
 */
38

  
39
function get_secret()
40
{
41
    if (is_chrome)
42
	return browser.runtime.getManifest().key.substring(0, 50);
43
    else
44
	return browser.runtime.getURL("dummy");
45
}
46

  
47
function extract_signed(signature, signed_data)
48
{
49
    const match = /^([1-9][0-9]{12}|0)_(.*)$/.exec(signed_data);
50
    if (!match)
51
	return {fail: "bad format"};
52

  
53
    const result = {time: parseInt(match[1]), data: match[2]};
54
    if (sign_data(result.data, result.time)[0] !== signature)
55
	result.fail = "bad signature";
56

  
57
    return result;
58
}
59

  
60
/*
61
 * Sign a given string for a given time. Time should be either 0 or in the range
62
 * 10^12 <= time < 10^13.
63
 */
64
function sign_data(data, time) {
65
    return [sha256(get_secret() + time + data), `${time}_${data}`];
66
}
67

  
68
/*
69
 * EXPORTS_START
70
 * EXPORT extract_signed
71
 * EXPORT sign_data
72
 * EXPORTS_END
73
 */
content/main.js
32 32
}
33 33

  
34 34
if (!is_privileged_url(document.URL)) {
35
    const reductor =
36
	  (ac, [_, sig, pol]) => ac[0] && ac || [extract_signed(sig, pol), sig];
37
    const matches = [...document.cookie.matchAll(/hachette-(\w*)=([^;]*)/g)];
38
    let [policy, signature] = matches.reduce(reductor, []);
35
    /* Signature valid for half an hour. */
36
    const min_time = new Date().getTime() - 1800 * 1000;
37
    let best_result = {time: -1};
38
    let policy = null;
39
    const extracted_signatures = [];
40
    for (const match of document.cookie.matchAll(/hachette-(\w*)=([^;]*)/g)) {
41
	const new_result = extract_signed(...match.slice(1, 3));
42
	if (new_result.fail)
43
	    continue;
39 44

  
40
    if (!policy || policy.url !== document.URL) {
41
	console.log("WARNING! Using default policy!!!");
45
	extracted_signatures.push(match[1]);
46

  
47
	if (new_result.time < Math.max(min_time, best_result.time))
48
	    continue;
49

  
50
	/* This should succeed - it's our self-produced valid JSON. */
51
	const new_policy = JSON.parse(decodeURIComponent(new_result.data));
52
	if (new_policy.url !== document.URL)
53
	    continue;
54

  
55
	best_result = new_result;
56
	policy = new_policy;
57
    }
58

  
59
    if (!policy) {
60
	console.warn("WARNING! Using default policy!!!");
42 61
	policy = {allow: false, nonce: gen_nonce()};
43 62
    }
44 63

  
45
    if (signature)
64
    for (const signature of extracted_signatures)
46 65
	document.cookie = `hachette-${signature}=; Max-Age=-1;`;
47 66

  
48 67
    handle_page_actions(policy.nonce);

Also available in: Unified diff