Offuscamento di un ID


85

Sto cercando un modo per crittografare / offuscare un ID intero in un altro intero. Più precisamente, ho bisogno di una funzione int F(int x), quindi

  • x <-> F (x) è una corrispondenza uno a uno (se x! = y, F (x)! = F (y))
  • dato F (x), è facile scoprire x - quindi F non è una funzione hash
  • dati x e F (x) è difficile / impossibile trovare F (y), qualcosa come x ^ 0x1234non funzionerà

Per chiarezza, non sto cercando una soluzione di crittografia forte, è solo offuscamento. Immagina un'applicazione web con URL come example.com/profile/1, example.com/profile/2ecc. I profili stessi non sono segreti, ma vorrei impedire ai guardoni occasionali di visualizzare / recuperare tutti i profili uno dopo l'altro, quindi preferirei nasconderli dietro qualcosa di simile example.com/profile/23423, example.com/profile/80980234ecc. i token archiviati nel database possono svolgere il lavoro abbastanza facilmente, sono curioso di sapere se ci sono alcuni semplici calcoli disponibili per questo.

Un requisito importante su cui non ero chiaro è che i risultati dovrebbero apparire "casuali", cioè, data una sequenza x,x+1,...,x+n, F(x),F(x+1)...F(x+n)non dovrebbero formare una progressione di alcun tipo.


Int F (int x) è un requisito o potrebbe essere int [2] F (int x)?
Eugen Rieck

@Eugen Rieck, idealmente, vorrei che x e F (x) fossero nell'intervallo numerico
georg

@ toon81, sì, la funzione sarà tenuta segreta
georg

Dato che hai detto che vorresti andare senza un token, significa che vuoi evitare qualsiasi tipo di tabella di ricerca?
Daniel Mošmondor

16
Amico, questa domanda è formulata perfettamente ed è esattamente quello che sto cercando. Bel lavoro.
Snekse

Risposte:


39

Offuscalo con una combinazione di 2 o 3 semplici metodi:

  • XOR
  • mescolare singoli bit
  • convertire in rappresentazione modulare (D.Knuth, Vol.2, Capitolo 4.3.2)
  • scegli 32 (o 64) sottoinsiemi di bit sovrapposti e bit XOR in ogni sottoinsieme (bit di parità di sottoinsiemi)
  • rappresentalo in un sistema numerico di lunghezza variabile e mescola le cifre
  • scegliere una coppia di numeri interi dispari xe yche sono inversi moltiplicativi l'uno dell'altro (modulo 2 32 ), quindi moltiplicare per xper offuscare e moltiplicare per yper ripristinare, tutte le moltiplicazioni sono modulo 2 32 (fonte: "Un uso pratico degli inversi moltiplicativi" di Eric Lippert )

Il metodo del sistema numerico a lunghezza variabile non obbedisce da solo ai requisiti di "progressione". Produce sempre brevi progressioni aritmetiche. Ma se combinato con qualche altro metodo, dà buoni risultati.

Lo stesso vale per il metodo di rappresentazione modulare.

Ecco un esempio di codice C ++ per 3 di questi metodi. L'esempio di bit shuffle può utilizzare alcune maschere e distanze diverse per essere più imprevedibili. Altri 2 esempi sono buoni per piccoli numeri (solo per dare l'idea). Dovrebbero essere estesi per offuscare correttamente tutti i valori interi.

// *** Numberic system base: (4, 3, 5) -> (5, 3, 4)
// In real life all the bases multiplied should be near 2^32
unsigned y = x/15 + ((x/5)%3)*4 + (x%5)*12; // obfuscate
unsigned z = y/12 + ((y/4)%3)*5 + (y%4)*15; // restore

// *** Shuffle bits (method used here is described in D.Knuth's vol.4a chapter 7.1.3)
const unsigned mask1 = 0x00550055; const unsigned d1 = 7;
const unsigned mask2 = 0x0000cccc; const unsigned d2 = 14;

// Obfuscate
unsigned t = (x ^ (x >> d1)) & mask1;
unsigned u = x ^ t ^ (t << d1);
t = (u ^ (u  >> d2)) & mask2;
y = u ^ t ^ (t << d2);

// Restore
t = (y ^ (y >> d2)) & mask2;
u = y ^ t ^ (t << d2);
t = (u ^ (u >> d1)) & mask1;
z = u ^ t ^ (t << d1);

// *** Subset parity
t = (x ^ (x >> 1)) & 0x44444444;
u = (x ^ (x << 2)) & 0xcccccccc;
y = ((x & 0x88888888) >> 3) | (t >> 1) | u; // obfuscate

t = ((y & 0x11111111) << 3) | (((y & 0x11111111) << 2) ^ ((y & 0x22222222) << 1));
z = t | ((t >> 2) ^ ((y >> 2) & 0x33333333)); // restore

La ringrazio per la risposta. Se potessi fornire alcuni esempi di pseudo-codice, sarebbe fantastico.
georg

3
@ thg435 Ho usato C ++ invece di pseudocodice. Non volevo fornire esempi non testati.
Evgeny Kluev

1
Quando provo il codice di base del sistema numerico sopra con x = 99, ottengo z = 44.
Harvey

@ Harvey: per ottenere un offuscatore reversibile, il prodotto di tutte le basi deve essere maggiore del numero da offuscare. In questo esempio 3 * 4 * 5 = 60, quindi qualsiasi numero più grande (come 99) non verrà necessariamente ripristinato allo stesso valore.
Evgeny Kluev

1
@ Harvey: Inoltre è possibile ottenere un prodotto di tutte le basi più piccolo ma molto vicino a 2 ^ 32, e quindi offuscare i valori rimanenti utilizzando una piccola tabella. In questo caso tutto rimane in numeri a 32 bit.
Evgeny Kluev

8

Vuoi che la trasformazione sia reversibile e non ovvia. Sembra una crittografia che accetta un numero in un determinato intervallo e produce un numero diverso nello stesso intervallo. Se l'intervallo è costituito da numeri a 64 bit, utilizza DES. Se il tuo intervallo è di 128 bit, usa AES. Se desideri un intervallo diverso, la soluzione migliore è probabilmente il cifrario Hasty Pudding , progettato per far fronte a diverse dimensioni di blocco e con intervalli di numeri che non si adattano perfettamente a un blocco, come da 100.000 a 999.999.


Cose interessanti, ma potrebbe essere un po 'difficile chiedere a qualcuno di implementare un codice che 1) non è stato testato bene e 2) non è stato testato bene perché è così difficile da capire :)
Maarten Bodewes

Grazie! Tuttavia, sto cercando di mantenerlo il più semplice possibile.
georg

Se non riesci a trovare un'implementazione di Hasty Pudding (hai solo bisogno di una delle dimensioni consentite), puoi facilmente implementare un semplice cifrario Feistel a 4 round ( en.wikipedia.org/wiki/Feistel_cipher ) in una dimensione di blocco pari. Continua a crittografare finché l'output non è nell'intervallo corretto, come con Hasty Pudding. Non sicuro, ma abbastanza da offuscare.
rossum

La NSA ha ora rilasciato il cifrario Speck che include versioni che includono blocchi di dimensioni a 32 bit e 48 bit. Può anche essere utile per offuscare i numeri con quelle dimensioni. La versione a 32 bit in particolare potrebbe essere utile.
rossum

5

L'offuscamento non è davvero sufficiente in termini di sicurezza.

Tuttavia, se stai cercando di contrastare lo spettatore occasionale, ti consiglio una combinazione di due metodi:

  • Una chiave privata che combini con l'ID xor'ing insieme
  • Rotazione delle punte di una certa quantità sia prima che dopo l'applicazione della chiave

Ecco un esempio (utilizzando uno pseudo codice):

  def F(x)
    x = x XOR 31415927       # XOR x with a secret key
    x = rotl(x, 5)           # rotate the bits left 5 times
    x = x XOR 31415927       # XOR x with a secret key again
    x = rotr(x, 5)           # rotate the bits right 5 times
    x = x XOR 31415927       # XOR x with a secret key again
    return x                 # return the value
  end

Non l'ho testato, ma penso che questo sia reversibile, dovrebbe essere veloce e non troppo facile per scoprire il metodo.


C'è anche l'aggiunta di un mod costante 2 ^ 32 (perché la rotazione del bit mi ha ricordato rot13, la funzione banalmente reversibile preferita da tutti).
ccoakley

Questo è davvero solo return x XOR rotr(31415927, 5)però, giusto? L'ultimo xor annulla il primo e le rotazioni si annullano a vicenda .. ovviamente anche qualsiasi catena di operazioni reversibili è reversibile, quindi soddisfa quella condizione.
Harold

Ho eseguito alcuni brevi test e sono soddisfatto che i risultati siano quelli previsti. Come ccoakley menziona, rot13 può essere usato al posto di rot5, qualsiasi rotazione funzionerà (avvertimento: 0> rot> integer-size) e potrebbe essere considerata un'altra chiave. Ci sono altre cose che potresti inserire qui, come il modulo come suggerisce, e purché siano reversibili come menzionato da Harold.
IAmNaN

1
Mi spiace, ma @harold è per lo più corretto: l'intera funzione è equivalente a x = x XOR F(0), o x = x XOR 3087989491, o x = x XOR rotr(31415927, 5). Il tuo primo e ultimo xor si negano a vicenda, quindi tutto ciò che stai facendo è xorizzare l'input spostato di bit con la chiave o, equivalentemente, xorizzare l'input con il tasto di cambio di bit. Nota che questo è vero anche se hai usato chiavi diverse per ogni fase: tutte le chiavi potrebbero essere composte in una singola chiave che può essere xorizzata con il testo in chiaro.
Nick Johnson

2
È anche peggio, è abbastanza facile dimostrare che qualsiasi catena di rotazioni di un offset costante e xors con una costante può essere condensata in una sola rotazione e un solo xor. È possibile combinare due rotazioni una dopo l'altra (aggiungere il loro offset), due xor dopo l'altra possono essere combinate (xor con l'xor delle due costanti) e una coppia xor / rot può essere scambiata in rot / xor applicando la stessa rotazione a la costante in xor.
Harold


3

Ho scritto del codice JS usando alcune delle idee in questo thread:

const BITS = 32n;
const MAX = 4294967295n;
const COPRIME = 65521n;
const INVERSE = 2166657316n;
const ROT = 6n;
const XOR1 = 10296065n; 
const XOR2 = 2426476569n;


function rotRight(n, bits, size) {
    const mask = (1n << bits) - 1n;
    // console.log('mask',mask.toString(2).padStart(Number(size),'0'));
    const left = n & mask;
    const right = n >> bits;
    return (left << (size - bits)) | right;
}

const pipe = fns => fns.reduce((f, g) => (...args) => g(f(...args)));

function build(...fns) {
    const enc = fns.map(f => Array.isArray(f) ? f[0] : f);
    const dec = fns.map(f => Array.isArray(f) ? f[1] : f).reverse();

    return [
        pipe(enc),
        pipe(dec),
    ]
}

[exports.encode, exports.decode] = build(
    [BigInt, Number],
    [i => (i * COPRIME) % MAX, i => (i * INVERSE) % MAX],
    x => x ^ XOR1,
    [x => rotRight(x, ROT, BITS), x => rotRight(x, BITS-ROT, BITS)],
    x => x ^ XOR2,
);

Produce alcuni buoni risultati come:

1 1352888202n 1 'mdh37u'
2 480471946n 2 '7y26iy'
3 3634587530n 3 '1o3xtoq'
4 2225300362n 4 '10svwqy'
5 1084456843n 5 'hxno97'
6 212040587n 6 '3i8rkb'
7 3366156171n 7 '1jo4eq3'
8 3030610827n 8 '1e4cia3'
9 1889750920n 9 'v93x54'
10 1017334664n 10 'gtp0g8'
11 4171450248n 11 '1wzknm0'
12 2762163080n 12 '19oiqo8'
13 1621319561n 13 'qtai6h'
14 748903305n 14 'cdvlhl'
15 3903018889n 15 '1sjr8nd'
16 3567473545n 16 '1mzzc7d'
17 2426613641n 17 '144qr2h'
18 1554197390n 18 'ppbudq'
19 413345678n 19 '6u3fke'
20 3299025806n 20 '1ik5klq'
21 2158182286n 21 'zoxc3y'
22 1285766031n 22 'l9iff3'
23 144914319n 23 '2ea0lr'
24 4104336271n 24 '1vvm64v'
25 2963476367n 25 '1d0dkzz'
26 2091060108n 26 'ykyob0'
27 950208396n 27 'fpq9ho'
28 3835888524n 28 '1rfsej0'
29 2695045004n 29 '18kk618'
30 1822628749n 30 'u559cd'
31 681777037n 31 'b9wuj1'
32 346231693n 32 '5q4y31'

Test con:

  const {encode,decode} = require('./obfuscate')

  for(let i = 1; i <= 1000; ++i) {
        const j = encode(i);
        const k = decode(j);
        console.log(i, j, k, j.toString(36));
   }

XOR1e XOR2sono solo numeri casuali compresi tra 0 e MAX. MAXè 2**32-1; dovresti impostarlo su qualunque cosa pensi che sarà il tuo ID più alto.

COPRIMEè un numero coprimo w / MAX. Penso che i numeri primi stessi siano coprimi con ogni altro numero (eccetto i multipli di se stessi).

INVERSEè quello difficile da capire. Questi post del blog non danno una risposta diretta, ma WolframAlpha può capirlo per te . Fondamentalmente, risolvi l'equazione (COPRIME * x) % MAX = 1per x.

La buildfunzione è qualcosa che ho creato per rendere più semplice la creazione di queste pipeline di codifica / decodifica. Puoi alimentare tutte le operazioni che desideri in [encode, decode]coppia. Queste funzioni devono essere uguali e opposte. Le XORfunzioni sono i loro complimenti, quindi non hai bisogno di un paio lì.


Ecco un'altra divertente involuzione :

function mixHalves(n) {
    const mask = 2n**12n-1n;
    const right = n & mask;
    const left = n >> 12n;
    const mix = left ^ right;
    return (mix << 12n) | right;
}

(presuppone numeri interi a 24 bit: basta cambiare i numeri per qualsiasi altra dimensione)


1
fantastico, grazie per la condivisione! A proposito, cos'è "32n"? Non l'ho mai visto prima.
georg

1
nè un suffisso numerico per BigInts . È una nuova funzionalità JS che ti consente di elaborare numeri davvero grandi. Avevo bisogno di usarlo perché sto moltiplicando per numeri molto grandi che potrebbero causare il temporaneo superamento di uno dei valori intermedi Number.MAX_SAFE_INTEGERe la perdita di precisione.
mpen

2

Fare qualsiasi cosa con i bit dell'ID che non li distrugga. Per esempio:

  • ruotare il valore
  • utilizzare la ricerca per sostituire alcune parti del valore
  • xo con un certo valore
  • bit di scambio
  • byte di scambio
  • rispecchia l'intero valore
  • rispecchiare una parte del valore
  • ... Usa la tua immaginazione

Per la decrittazione, fai tutto in ordine inverso.

Crea un programma che "crittografa" alcuni valori interessanti e inseriscili in una tabella che puoi esaminare. Avere lo stesso programma TESTARE la routine di crittografia / decrittografia CON tutti i set di valori che si desidera avere nel sistema.

Aggiungi elementi all'elenco sopra nelle routine finché i tuoi numeri non ti sembreranno opportunamente alterati.

Per qualsiasi altra cosa, procurati una copia del libro .


Ciò che descrivi sono gli elementi costitutivi di un codice a blocchi. Ha più senso usarne uno esistente che inventarne uno tuo.
Nick Johnson

@ NickJohnson Lo so, hai cliccato sul link nell'ultima riga del mio post?
Daniel Mošmondor

Non sono riuscito a mettere insieme una combinazione rotl / xor dando risultati che sembrano abbastanza "casuali" (vedere l'aggiornamento). Qualche suggerimento?
georg

@ DanielMošmondor So a cosa ti stai collegando, ma questo non cambia il fatto che inizialmente gli stai suggerendo di costruire qualcosa da solo, quando ha molto più senso usarne uno esistente?
Nick Johnson

@NickJohnson ovviamente OP non vuole utilizzare la crittografia esistente, poiché vuole imparare o non apprendere nuove API. Mi posso assolutamente riferire a questo.
Daniel Mošmondor

2

Ho scritto un articolo sulle permutazioni sicure con i cifrari a blocchi , che dovrebbe soddisfare i tuoi requisiti come indicato.

Suggerirei, tuttavia, che se desideri identificatori difficili da indovinare, dovresti semplicemente usarli in primo luogo: generare UUID e utilizzarli come chiave primaria per i tuoi record in primo luogo - non è necessario essere in grado per convertire da e verso un ID "reale".


2
@ thg435 Se sei interessato a questo approccio, un termine di ricerca utile è "Format-Preserving Encryption". La pagina di wikipedia copre il documento Black / Rogaway menzionato nell'articolo di Nick, così come gli sviluppi più recenti. Ho utilizzato con successo FPE per qualcosa di simile a quello che stai facendo; anche se nel mio caso ho aggiunto alcuni bit oltre all'id che ho usato per un leggero controllo di validità.
Paul Du Bois

1

Non sono sicuro di quanto "difficile" debba essere, quanto veloce o quanto poca memoria utilizzare. Se non hai vincoli di memoria puoi fare un elenco di tutti i numeri interi, mescolarli e usare quell'elenco come mappatura. Tuttavia, anche per un numero intero a 4 byte, è necessaria molta memoria.

Tuttavia, questo potrebbe essere reso più piccolo, quindi invece di mappare tutti gli interi dovresti mappare solo 2 (o nel caso peggiore 1) byte e applicarlo a ogni gruppo nell'intero. Quindi, utilizzando 2 byte un numero intero sarebbe (gruppo1) (gruppo2), mapperai ogni gruppo attraverso la mappa casuale. Ma ciò significa che se modifichi solo group2, la mappatura per group1 rimarrà la stessa. Questo potrebbe essere "risolto" mappando bit diversi a ciascun gruppo.

Quindi, * (group2) potrebbe essere (bit 14,12,10,8,6,4,2,0) quindi, l'aggiunta di 1 cambierebbe sia group1 che group2 .

Tuttavia, questa è solo sicurezza per oscurità, chiunque possa inserire numeri nella tua funzione (anche se mantieni segreta la funzione) potrebbe capirlo abbastanza facilmente.


A seconda dei vincoli del sistema questo probabilmente non funzionerà, perché se puoi invertire F (x) in x allora dovresti avere a disposizione la permutazione, dalla quale potresti facilmente calcolare F (y) dato qualsiasi arbitrario y.
templatetypedef

@templatetypedef Come ho detto, questa è solo sicurezza per oscurità. La permutazione dovrebbe essere nota, ma potresti vedere la permutazione come la "chiave". Il problema più grande qui è che l'OP sembra voler essere in grado di crittografare tutti i messaggi in un set (uno piccolo) in cui il messaggio crittografato dovrebbe rientrare nello stesso set e questo dovrebbe essere valido per tutti i messaggi nel set.
Roger Lindsjö

Grazie. Sto cercando di evitare qualsiasi tabella di ricerca.
georg

1

Genera una chiave simmetrica privata da utilizzare nella tua applicazione e crittografa il tuo numero intero con essa. Questo soddisferà tutti e tre i requisiti, incluso il più difficile n.3: uno dovrebbe indovinare la tua chiave per rompere il tuo schema.


thg435 ha chiesto da intero a intero (e per quanto ho capito dovrebbe funzionare per tutti i numeri interi). Potete suggerire un algoritmo di chiave privata che abbia queste proprietà?
Roger Lindsjö

1

Quello che stai descrivendo qui sembra essere l'opposto di una funzione unidirezionale: è facile da invertire ma super difficile da applicare. Un'opzione sarebbe quella di utilizzare un algoritmo di crittografia a chiave pubblica standard e disponibile in commercio in cui si corregge una chiave pubblica (segreta, scelta a caso) che si mantiene segreta e una chiave privata che si condivide con il mondo. In questo modo, la tua funzione F (x) sarebbe la crittografia di x utilizzando la chiave pubblica. È quindi possibile decrittografare facilmente F (x) in x utilizzando la chiave di decrittografia privata. Nota che i ruoli della chiave pubblica e privata sono invertiti qui: dai la chiave privata a tutti in modo che possano decrittografare la funzione, ma mantieni la chiave pubblica segreta sul tuo server. Quel modo:

  1. La funzione è una biiezione, quindi è invertibile.
  2. Dato F (x), x è calcolabile in modo efficiente.
  3. Dati x e F (x), è estremamente difficile calcolare F (y) da y, poiché senza la chiave pubblica (supponendo che si utilizzi uno schema di crittografia forte crittograficamente) non esiste un modo fattibile per crittografare i dati, anche se il privato la chiave di decrittazione è nota.

Questo ha molti vantaggi. Innanzitutto, puoi essere certo che il sistema crittografico è sicuro, poiché se utilizzi un algoritmo consolidato come RSA, non devi preoccuparti dell'insicurezza accidentale. In secondo luogo, ci sono già librerie là fuori per farlo, quindi non è necessario programmare molto e puoi essere immune agli attacchi di canale laterale. Infine, puoi consentire a chiunque di andare e invertire F (x) senza che nessuno sia effettivamente in grado di calcolare F (x).

Un dettaglio: non dovresti assolutamente usare il tipo int standard qui. Anche con interi a 64 bit, ci sono così poche combinazioni possibili che un utente malintenzionato potrebbe semplicemente provare a invertire tutto fino a quando non trova la crittografia F (y) per alcuni y anche se non ha la chiave. Suggerirei di utilizzare qualcosa come un valore di 512 bit, poiché anche un attacco di fantascienza non sarebbe in grado di forzare questo.

Spero che sia di aiuto!


Ma sembra che thg435 richieda una crittografia in grado di crittografare un piccolo insieme di messaggi (messaggi a 4 byte) nello stesso insieme di messaggi e la crittografia dovrebbe funzionare per tutti i messaggi.
Roger Lindsjö

La ringrazio per la risposta. Usare un framework di crittografia completo è forse il modo migliore per farlo, ma un po 'troppo "pesante" per le mie esigenze.
georg

1

Se xorè accettabile per tutto tranne che per inferenza F(y)data xe F(x)quindi penso che tu possa farlo con un sale . Prima scegli una funzione segreta unidirezionale. Ad esempio S(s) = MD5(secret ^ s). Quindi F(x) = (s, S(s) ^ x)dove sviene scelto a caso. L'ho scritto come una tupla ma puoi combinare le due parti in un numero intero, ad es F(x) = 10000 * s + S(s) ^ x. La decrittazione estrae snuovamente il sale e utilizza F'(F(x)) = S(extract s) ^ (extract S(s)^x). Dato xe F(x)puoi vedere s(anche se è leggermente offuscato) e puoi dedurre S(s)ma per qualche altro utente ycon un salt casuale diverso che tl'utente conosce F(x)non può trovare S(t).


Grazie, ma questo non sembra abbastanza casuale per me (vedi l'aggiornamento)
georg

Il sale viene scelto in modo casuale e anche l'hash S(s)avrà un aspetto casuale, quindi F(x)non avrà alcun tipo di progressione.
Ben Jackson
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.