Here’s a script to generate an encrypted file that needs n/m keys (n < m) to decrypt the file.

Use:

multicrypt file-to-encrypt minimal-key-number key-1 key-2 ...

The keys are taken from the GPG key store.

It encrypts all possible combinations that can decode the data. The total combination count is: nCr or n! / (r! * (n - r)!) where n is the total number of keys and r is the minimal number specified to decrypt. You must decrypt minimal-key-number of times to get the decrypted data out.

Save as multicrypt

#! /usr/bin/env bash

# ARG_HELP([Encrypt a file such that it requires multiple keys to decrypt.])
# ARG_POSITIONAL_SINGLE([file],[File to encrypt.])
# ARG_POSITIONAL_SINGLE([depth],[Amount of keys required to decrypt.])
# ARG_OPTIONAL_SINGLE([name],[n],[Name of target file.],[all-keys])
# ARG_POSITIONAL_INF([keys],[Keys to encrypt with.])
# ARG_POSITIONAL_DOUBLEDASH([])
# ARGBASH_GO()
# needed because of Argbash --> m4_ignore([
### START OF CODE GENERATED BY Argbash v2.10.0 one line above ###
# Argbash is a bash code generator used to get arguments parsing right.
# Argbash is FREE SOFTWARE, see https://argbash.io for more info


die()
{
    local _ret="${2:-1}"
    test "${_PRINT_HELP:-no}" = yes && print_help >&2
    echo "$1" >&2
    exit "${_ret}"
}


begins_with_short_option()
{
    local first_option all_short_options='hn'
    first_option="${1:0:1}"
    test "$all_short_options" = "${all_short_options/$first_option/}" && return 1 || return 0
}

# THE DEFAULTS INITIALIZATION - POSITIONALS
_positionals=()
_arg_keys=()
# THE DEFAULTS INITIALIZATION - OPTIONALS
_arg_name="all-keys"


print_help()
{
    printf '%s\n' "Encrypt a file such that it requires multiple keys to decrypt."
    printf 'Usage: %s [-h|--help] [-n|--name <arg>] [--] <file> <depth> [<keys-1>] ... [<keys-n>] ...\n' "$0"
    printf '\t%s\n' "<file>: File to encrypt."
    printf '\t%s\n' "<depth>: Amount of keys required to decrypt."
    printf '\t%s\n' "<keys>: Keys to encrypt with."
    printf '\t%s\n' "-h, --help: Prints help"
    printf '\t%s\n' "-n, --name: Name of target file. (default: 'all-keys')"
}


parse_commandline()
{
    _positionals_count=0
    while test $# -gt 0
    do
        _key="$1"
        if test "$_key" = '--'
        then
            shift
            test $# -gt 0 || break
            _positionals+=("$@")
            _positionals_count=$((_positionals_count + $#))
            shift $(($# - 1))
            _last_positional="$1"
            break
        fi
        case "$_key" in
            -h|--help)
                print_help
                exit 0
                ;;
            -h*)
                print_help
                exit 0
                ;;
            -n|--name)
                test $# -lt 2 && die "Missing value for the optional argument '$_key'." 1
                _arg_name="$2"
                shift
                ;;
            --name=*)
                _arg_name="${_key##--name=}"
                ;;
            -n*)
                _arg_name="${_key##-n}"
                ;;
            *)
                _last_positional="$1"
                _positionals+=("$_last_positional")
                _positionals_count=$((_positionals_count + 1))
                ;;
        esac
        shift
    done
}


handle_passed_args_count()
{
    local _required_args_string="'file' and 'depth'"
    test "${_positionals_count}" -ge 2 || _PRINT_HELP=yes die "FATAL ERROR: Not enough positional arguments - we require at least 2 (namely: $_required_args_string), but got only ${_positionals_count}." 1
}


assign_positional_args()
{
    local _positional_name _shift_for=$1
    _positional_names="_arg_file _arg_depth "
    _our_args=$((${#_positionals[@]} - 2))
    for ((ii = 0; ii < _our_args; ii++))
    do
        _positional_names="$_positional_names _arg_keys[$((ii + 0))]"
    done

    shift "$_shift_for"
    for _positional_name in ${_positional_names}
    do
        test $# -gt 0 || break
        eval "$_positional_name=\${1}" || die "Error during argument parsing, possibly an Argbash bug." 1
        shift
    done
}

parse_commandline "$@"
handle_passed_args_count
assign_positional_args 1 "${_positionals[@]}"

# OTHER STUFF GENERATED BY Argbash

### END OF CODE GENERATED BY Argbash (sortof) ### ])
# [ <-- needed because of Argbash

set -Eeuo pipefail

depth="$_arg_depth"
file="$_arg_file"

if [ "$depth" -eq 0 ]; then
    echo "Depth must be larger than zero"
    exit 1
fi

command -v gpg2 >& /dev/null || {
    echo "gpg2 not found, exiting"
    exit 2
}

recipientIndex=0
for recipient in "${_arg_keys[@]}"; do
    output="$recipient-for-${file%.gpg}.gpg"
    gpg2 --encrypt --batch --recipient "$recipient" --output "$output" "$file"
    if [ "$depth" -gt 1 ]; then
        clone=("${_arg_keys[@]:((recipientIndex+1))}")
        "$0" "$output" "$((depth-1))" --name "need-$recipient" "${clone[@]}"
        ((recipientIndex += 1))
        rm "$output"
    fi
done

# ] <-- needed because of Argbash