Skip to content

Instantly share code, notes, and snippets.

@syncom
Last active April 30, 2021 01:26
Show Gist options
  • Save syncom/f6fd24d0b3bddfcef9abb2fd34ee50af to your computer and use it in GitHub Desktop.
Save syncom/f6fd24d0b3bddfcef9abb2fd34ee50af to your computer and use it in GitHub Desktop.

How to Distribute Password Halves for Separation of Duty

Problem:

We have 5 passwords: a, b, c, d, e. It needs exactly two distinct passwords to open a safe. Each of the passwords is split into two halve - a prefix and a suffix. Let them be

    a1, a2; b1, b2; c1, c2; d1, d2; e1, e2

where a == a1||a2, etc.

We would like to distribute all password halves to N different people. What is the minimum N such that it requires at least three people to open the safe?

Answer

The minimum N is 4. A solution is as follows.

#!/usr/bin/env python3

'''
We have 5 passwords: a, b, c, d, e. It needs exactly two distinct passwords
to open a safe. Each of the passwords is split into two halve - a prefix and
a suffix. Let them be
    a1, a2; b1, b2; c1, c2; d1, d2; e1, e2
where a == a1||a2, etc.
We would like to distribute all password halves to N different people. What
is the minimum N such that it requires at least three people to open the
safe?
'''

import itertools


def partition_n(collection, n):
    '''Partition collection into n subsets
    '''
    if n < 1 or n > len(collection):
        print('len of set: {0}, num of parts: {1}'.format(len(collection), n))
        raise ValueError('No partition exists')

    if n == len(collection):
        yield [[c] for c in collection]
        return

    if n == 1 or len(collection) == 1:
        yield [collection]
        return

    first = collection[0]
    for smaller in partition_n(collection[1:], n-1):
        yield [ [first] ] + smaller

    for smaller in partition_n(collection[1:], n):
        for i, subset in enumerate(smaller):
            yield smaller[:i] + [[first] + subset] + smaller[i+1:]
    return


def can_construct_password(password_halves, password):
    '''Return True if the list of password_halves can construct password
    Example: password_halves [('a', 1), ('a', 2)] can construct password a
    '''
    count = 0
    for ph in password_halves:
        count += 1 if ph[0] == password else 0
    assert count <= 2
    return count == 2


def can_open_safe(password_halves, password_list):
    '''Return True if password_halves can construct at least 2 passwords in
    password_list
    '''
    count = 0
    for p in password_list:
        count += 1 if can_construct_password(password_halves, p) else 0

    return True if count >= 2 else False


def require_3_to_open(password_distribution, password_list):
    '''Return True if no two people on the password_distribution list can
       open the safe
       Example of password_distribution:
       [[('d', 2)], [('e', 1)], [('a', 1), ('a', 2), ('b', 1), ('b', 2), ('c', 1), ('c', 2), ('d', 1), ('e', 2)]]
    '''
    combos = itertools.combinations(password_distribution, 2)
    for idx, combo in enumerate(combos):
        password_halves = combo[0] + combo[1]
        if can_open_safe(password_halves, password_list):
            return False
    print('require_3_to_open: ', password_distribution)
    return True


def is_atleast3_of_k(password_halves, password_list, k):
    '''Return True if there's a k-partition of password_halves that requires
       3 people to open the safe.
    '''
    partitions = partition_n(password_halves, k)
    for idx, password_distribution in enumerate(partitions):
        if require_3_to_open(password_distribution, password_list):
            return True;

    return False

if __name__ == '__main__':
    '''Check if the list of password halves can induce a 3-of-k situation.
    Password halves are of the form (password_string, i), where i=1, 2.
    '''
    password_halves = [('a', 1), ('a', 2),
                       ('b', 1), ('b', 2),
                       ('c', 1), ('c', 2),
                       ('d', 1), ('d', 2),
                       ('e', 1), ('e', 2)]
    password_list = [p[0] for p in password_halves[::2]]
    print('**********************')
    print('password halves: ', password_halves)
    print('**********************')
    for k in range(3, 11):
        if is_atleast3_of_k(password_halves, password_list, k):
            print('Found a 3-of-{} condition above.'.format(k))
        else:
            print('Cannot form a 3-of-{} condition.'.format(k))

Running the above code gives us

 $ python3 password_distrbution.py 
**********************
password halves:  [('a', 1), ('a', 2), ('b', 1), ('b', 2), ('c', 1), ('c', 2), ('d', 1), ('d', 2), ('e', 1), ('e', 2)]
**********************
Cannot form a 3-of-3 condition.
require_3_to_open:  [[('a', 2), ('b', 1)], [('b', 2), ('c', 2), ('d', 1)], [('a', 1), ('d', 2), ('e', 1)], [('c', 1), ('e', 2)]]
Found a 3-of-4 condition above.
require_3_to_open:  [[('a', 1)], [('a', 2), ('b', 1)], [('b', 2), ('c', 2), ('d', 1)], [('d', 2), ('e', 1)], [('c', 1), ('e', 2)]]
Found a 3-of-5 condition above.
require_3_to_open:  [[('a', 1)], [('a', 2)], [('b', 1)], [('b', 2), ('c', 2), ('d', 1)], [('d', 2), ('e', 1)], [('c', 1), ('e', 2)]]
Found a 3-of-6 condition above.
require_3_to_open:  [[('a', 1)], [('a', 2)], [('b', 1)], [('b', 2)], [('c', 2), ('d', 1)], [('d', 2), ('e', 1)], [('c', 1), ('e', 2)]]
Found a 3-of-7 condition above.
require_3_to_open:  [[('a', 1)], [('a', 2)], [('b', 1)], [('b', 2)], [('c', 1)], [('c', 2), ('d', 1)], [('d', 2)], [('e', 1), ('e', 2)]]
Found a 3-of-8 condition above.
require_3_to_open:  [[('a', 1)], [('a', 2)], [('b', 1)], [('b', 2)], [('c', 1)], [('c', 2)], [('d', 1)], [('d', 2)], [('e', 1), ('e', 2)]]
Found a 3-of-9 condition above.
require_3_to_open:  [[('a', 1)], [('a', 2)], [('b', 1)], [('b', 2)], [('c', 1)], [('c', 2)], [('d', 1)], [('d', 2)], [('e', 1)], [('e', 2)]]
Found a 3-of-10 condition above.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment