def runOneAttack(guessedCol, knownCols, attack, table, numClaims): # -------------- Attack phase ------------------ # And now run the attack for some fraction of the attackable cells if v: print(f"RunOneAttack with guessed '{guessedCol}', known {knownCols}") allCols = [guessedCol] + list(knownCols) sql = "SELECT " sql += comma_ize(allCols) sql += str(f"count(*) FROM {table} ") sql += makeGroupBy(allCols) query = dict(sql=sql) attack.askAttack(query) reply = attack.getAttack() if 'error' in reply: doQueryErrorAndExit(reply,attack) # Build a dict out of the knownCols values, and remember the index # for cases where the knownCols has a single guessedCol value s = {} ans = reply['answer'] for r in range(len(ans)): # I want a 'foo'.join(thing) here, but need to deal with fact that # the value might not be a string key = '' for i in range(1,len(allCols)): key += '::' + str(f"{ans[r][i]}") if key in s: s[key] = -1 else: s[key] = r for key,r in s.items(): if r == -1: continue # This is a potential inference spec = {} known = [] row = ans[r] for i in range(1,len(allCols)): known.append({'col':allCols[i],'val':row[i]}) spec['known'] = known if row[0] is None: pp.pprint(ans) spec['guess'] = [{'col':guessedCol,'val':row[0]}] attack.askClaim(spec) while True: reply = attack.getClaim() numClaims += 1 if v: pp.pprint(reply) if reply['stillToCome'] == 0: break return numClaims
def dumb_list_linkability_attack(params): """ Dumb List attack for the Linkability criteria. All it does is request rows with all columns from the anonymized link database. The attack succeeds if the anonymized database returns rows that single out users, and fails otherwise. It is designed to work against raw and pseudonymized data. NOTE: This is effectively the same attack as with singling out dumb list.""" attack = gdaAttack(params) # ------------------- Exploration Phase ------------------------ # We need to know the columns that are in the anonymized database # and in the raw database. It is these columns that we can attack. # (Note that pseudonymization schemes typically delete some columns.) table = attack.getAttackTableName() rawColNames = attack.getColNames(dbType='rawDb') anonColNames = attack.getColNames(dbType='anonDb') colNames = list(set(rawColNames) & set(anonColNames)) # ------------------- Prior Knowledge Phase -------------------- # This attack doesn't require any prior knowledge # ------------------- Attack Phase ----------------------------- query = {} sql = "SELECT " sql += comma_ize(colNames) sql += str(f"count(*) FROM {table} ") sql += makeGroupBy(colNames) sql += " HAVING count(*) = 1 ORDER BY count(*) LIMIT 100" query['sql'] = sql print("-------------------- Attack query:") print(sql) attack.askAttack(query) reply = attack.getAttack() if v: print("-------------------- Attack reply:") if v: pp.pprint(reply) # ------------------- Claims Phase ---------------------------- if 'answer' not in reply: print("ERROR: reply to claim query contains no answer") pp.pprint(reply) attack.cleanUp() sys.exit() for row in reply['answer']: spec = {} guess = [] for i in range(len(colNames)): guess.append({'col': colNames[i], 'val': row[i]}) spec['guess'] = guess attack.askClaim(spec) if v: print("------------------- Attack claims:") while True: reply = attack.getClaim() if v: pp.pprint(reply) if reply['stillToCome'] == 0: break # ------------------- Scores Phase ---------------------------- attackResult = attack.getResults() sc = gdaScores(attackResult) score = sc.getScores() if v: pp.pprint(score) attack.cleanUp() final = finishGdaAttack(params, score) pp.pprint(final)
def dumb_list_inference_attack(params): """ Dumb List attack for the Inference criteria. In an inference attack, there are 'known' column values, and 'guessed' column values. An inference claim succeeds when all users with the known column values have the same guessed column values. There only needs to be one such user, so we can try making inferences on all columns by using all the other columns as known values. """ attack = gdaAttack(params) # ------------------- Exploration Phase ------------------------ # We need to know the columns that are in the anonymized database # and in the raw database. It is these columns that we can attack. # (Note that pseudonymization schemes typically delete some columns.) table = attack.getAttackTableName() rawColNames = attack.getColNames(dbType='rawDb') anonColNames = attack.getColNames(dbType='anonDb') colNames = list(set(rawColNames) & set(anonColNames)) # Get the total number of rows so that we can later determine fraction # of cells per column that are susceptible sql = str(f"SELECT count(*) FROM {table}") if v: print(sql) query = dict(db="raw", sql=sql) attack.askExplore(query) reply = attack.getExplore() if 'error' in reply: doQueryErrorAndExit(reply, attack) totalRows = reply['answer'][0][0] # ------------------- Prior Knowledge Phase -------------------- # This attack doesn't require any prior knowledge # ------------------- Attack Phase ----------------------------- # I'm going to attack each (guessed) column by using the remaining # columns as the known colums. In the following, I loop through # attack and claims for each guessed column. for guessedCol in colNames: remainingCols = [x for x in colNames if x != guessedCol] # -------------- Attack phase ------------------ # And now run the attack for some fraction of the attackable cells sql = "SELECT " sql += comma_ize(remainingCols) sql += str(f"max({guessedCol}) FROM {table} WHERE ") sql += makeInNotNullConditions(remainingCols) sql += makeGroupBy(remainingCols) sql += str(f" HAVING count(DISTINCT {guessedCol}) = 1 ") sql += str(f"ORDER BY 1 LIMIT 20") if v: print(sql) query = dict(sql=sql) attack.askAttack(query) reply = attack.getAttack() if 'error' in reply: # For this attack, cloak can't deal with max(text_col), # so just continue without claims continue # -------------- Claims phase ------------------ for row in reply['answer']: spec = {} known = [] for i in range(len(remainingCols)): known.append({'col': remainingCols[i], 'val': row[i]}) spec['known'] = known i = len(remainingCols) spec['guess'] = [{'col': guessedCol, 'val': row[i]}] attack.askClaim(spec) while True: reply = attack.getClaim() if v: pp.pprint(reply) if reply['stillToCome'] == 0: break # ------------------- Scores Phase ---------------------------- attackResult = attack.getResults() sc = gdaScores(attackResult) # New we need to assign susceptibility scores, which means making # some explore queries for guessedCol in colNames: remainingCols = [x for x in colNames if x != guessedCol] if len(remainingCols) > 20: remainingCols = remainingCols[:20] # -------------- More exploration phase ------------------ # First find out how many of the cells are attackable sql = "SELECT sum(rows) FROM (SELECT " sql += comma_ize(remainingCols) sql += str(f"count(*) AS rows FROM {table} ") sql += makeGroupBy(remainingCols) sql += str(f" HAVING count(DISTINCT {guessedCol}) = 1) t") if v: print("-------------------- Explore query:") if v: print(sql) query = dict(db="raw", sql=sql) attack.askExplore(query) reply = attack.getExplore() if 'error' in reply: doQueryErrorAndExit(reply, attack) numRows = reply['answer'][0][0] if v: print("-------------------- Explore reply:") if v: pp.pprint(reply) susValue = numRows / totalRows sc.assignColumnSusceptibility(guessedCol, susValue) score = sc.getScores() if v: pp.pprint(score) final = finishGdaAttack(params, score) attack.cleanUp() pp.pprint(final)
def dumb_list_singling_out_attack(params): """ Dumb List attack for the Singling Out criteria. All it does is request rows with all columns from the anonymized database. The attack succeeds if the anonymized database returns rows that single out users, and fails otherwise. It is designed to work against raw and pseudonymized data.""" attack = gdaAttack(params) # ------------------- Exploration Phase ------------------------ # We need to know the columns that are in the anonymized database # and in the raw database. It is these columns that we can attack. # (Note that pseudonymization schemes can delete some columns.) table = attack.getAttackTableName() rawColNames = attack.getColNames(dbType='rawDb') anonColNames = attack.getColNames(dbType='anonDb') uid = attack.getUidColName() colNamesAll = list(set(rawColNames) & set(anonColNames)) if v: print(f"Use columns: {colNamesAll}") # The cloak can't handle queries with a large number of columns, # so we split up the attack into groups of 5 columns each. Each group # contains the uid column, so that we are sure that the resulting # answer pertains to a single user. groupSize = 5 minAttacksPerGroup = 5 groups = [] colsWithoutUid = colNamesAll.copy() colsWithoutUid.remove(uid) if v: print(colNamesAll) if v: print(colsWithoutUid) index = 0 while (1): if index >= len(colsWithoutUid): break endIndex = index + groupSize - 1 nextGroup = colsWithoutUid[index:endIndex] nextGroup.append(uid) groups.append(nextGroup) index += groupSize - 1 # This will give us around 100 attack queries total: numAttacksPerGroup = min(int(100 / len(groups)) + 1, minAttacksPerGroup) if v: pp.pprint(groups) # ------------------- Prior Knowledge Phase -------------------- # This attack doesn't require any prior knowledge # ------------------- Attack Phase ----------------------------- for colNames in groups: query = {} sql = "SELECT " sql += comma_ize(colNames) sql += str(f"count(*) FROM {table} WHERE ") sql += makeInNotNullConditions(colNames) sql += makeGroupBy(colNames) sql += " HAVING count(*) = 1 ORDER BY uid " sql += str(f" LIMIT {numAttacksPerGroup} ") query['sql'] = sql print("-------------------- Attack query:") print(sql) attack.askAttack(query) reply = attack.getAttack() if v: print("-------------------- Attack reply:") if v: pp.pprint(reply) # ------------------- Claims Phase ---------------------------- if 'answer' not in reply: print("ERROR: reply to claim query contains no answer") pp.pprint(reply) attack.cleanUp() sys.exit() for row in reply['answer']: spec = {} guess = [] for i in range(len(colNames)): guess.append({'col': colNames[i], 'val': row[i]}) spec['guess'] = guess attack.askClaim(spec) if v: print("------------------- Attack claims:") while True: reply = attack.getClaim() if v: pp.pprint(reply) if reply['stillToCome'] == 0: break # ------------------- Scores Phase ---------------------------- attackResult = attack.getResults() sc = gdaScores(attackResult) score = sc.getScores() if v: pp.pprint(score) attack.cleanUp() final = finishGdaAttack(params, score) pp.pprint(final)
def diffix_infer_1_attack(params): ''' This is an inference attack against Diffix In this attack, we find attribute groups where the inference conditions exist (one one guessed column value exists for some set of one or more known column values). This is designed to work against Diffix and Full K-anonymity at least. ''' attack = gdaAttack(params) # ------------------- Exploration Phase ------------------------ # We need to know the columns that are in the anonymized database # and in the raw database. It is these columns that we can attack. table = attack.getAttackTableName() rawColNames = attack.getColNames(dbType='rawDb') anonColNames = attack.getColNames(dbType='anonDb') colNames = list(set(rawColNames) & set(anonColNames)) if v: print(f"Common columns are: {colNames}") # Get the total number of rows so that we can later determine fraction # of cells per column that are susceptible sql = str(f"SELECT count(*) FROM {table}") query = dict(db="rawDb",sql=sql) attack.askExplore(query) reply = attack.getExplore() if 'error' in reply: doQueryErrorAndExit(reply,attack) totalRows = reply['answer'][0][0] if v: print(f"Total Rows: {totalRows}") # There is really no point in trying to find instances of # inference where the guessed column has a large number of values. # In these cases, the chances of finding an inference instance is # very low. We (arbitrarily for now) set the threshold for this at 10 # By the same token, an attack where the known column has a majority # values that are distinct to a single user won't work for an attack, # because in the case of Diffix, they will be low-count filtered, and # in the case of Full K-anonymity, they may be aggregated # So we record the number of distinct values per column. (In practice, # this would not be known exactly, but the attacker can be assumed to # have a reasonable guess just based on knowledge of the column.) distincts = {} guessableCols = [] for col in colNames: sql = str(f"SELECT count(DISTINCT {col}) FROM {table}") query = dict(db="rawDb",sql=sql) attack.askAttack(query) reply = attack.getAttack() if 'error' in reply: doQueryErrorAndExit(reply,attack) totalDistinct = reply['answer'][0][0] distincts[col] = totalDistinct if totalDistinct <= 10: guessableCols.append(col) if v: print(f"Distincts: {distincts}") if v: print(f"guessableCols: {guessableCols}") # ------------------- Prior Knowledge Phase -------------------- # This attack doesn't require any prior knowledge for guessedCol in guessableCols: numClaims = 0 remainingCols = [x for x in colNames if x != guessedCol] # We want to try various combinations of the remaining columns, # and try the attack if the ratio of distinct values (or expected # distinct value combinations) is not too high unusedCombinations = 0 for num in range(len(remainingCols)): if unusedCombinations > 1000: # If we don't find a useable combination 1000 # consecutive times, then give up break if numClaims > 25: break combs = itertools.combinations(remainingCols,num+1) while True: if unusedCombinations > 1000: break if numClaims > 25: break try: knownCols = next(combs) except: break totalDistinct = 1 for c in knownCols: totalDistinct *= distincts[c] if v: print(f"totalDistinct: {totalDistinct} " "from known columns {knownCols}") if (totalDistinct / totalRows) > 0.8: unusedCombinations += 1 continue unusedCombinations = 0 numClaims = runOneAttack(guessedCol, knownCols, attack, table, numClaims) # ------------------- Scores Phase ---------------------------- attackResult = attack.getResults() sc = gdaScores(attackResult) # New we need to assign susceptibility scores, which means making # some explore queries for guessedCol in colNames: remainingCols = [x for x in colNames if x != guessedCol] # -------------- More exploration phase ------------------ # First find out how many of the cells are attackable sql = "SELECT sum(rows) FROM (SELECT " sql += comma_ize(remainingCols) sql += str(f"count(*) AS rows FROM {table} ") sql += makeGroupBy(remainingCols) sql += str(f" HAVING count(DISTINCT {guessedCol}) = 1) t") if v: print("-------------------- Explore query:") if v: print(sql) query = dict(db="raw",sql=sql) attack.askExplore(query) reply = attack.getExplore() if 'error' in reply: doQueryErrorAndExit(reply,attack) numRows = reply['answer'][0][0] if v: print("-------------------- Explore reply:") if v: pp.pprint(reply) susValue = numRows / totalRows sc.assignColumnSusceptibility(guessedCol,susValue) # Get average score (default behavior) score = sc.getScores() if v: pp.pprint(score) score = sc.getScores(numColumns=1) if v: pp.pprint(score) attack.cleanUp(cleanUpCache=False) final = finishGdaAttack(params,score) pp.pprint(final)