Skip to content

LSBHideStrInNNModel

stegobox.codec.LSBHideStrInNNModel

Bases: BaseCodec

LSBHideStrInNNModel - steganography algorithm that hides string in neural network models.

Originally implemented in gaborvecsei/Neural-Network-Steganography

Source code in stegobox/codec/hide_str_in_nn_model/lsb_hide_str_in_nnmodel.py
class LSBHideStrInNNModel(BaseCodec):
    """LSBHideStrInNNModel - steganography algorithm that hides string in neural
    network models.

    Originally implemented in
    [gaborvecsei/Neural-Network-Steganography](https://github.com/gaborvecsei/Neural-Network-Steganography)
    """

    def __init__(self, num_bits: int = 8) -> None:
        """To initialize the class and specify the number of bits to use.

        Args:
            num_bits: The number of bits to hide secret information.
        """
        super().__init__()
        self.bit_use = num_bits

    def payload_in_bytes(self, payload: str) -> list[np.uint8]:
        """Transform the string into bytes, example 'A' -> 65"""
        payloadb = payload.encode("utf-8")
        payloadsq = np.array([n for n in payloadb], dtype=np.uint8)
        return list(np.unpackbits(payloadsq))

    def reconstruct_bytes(self, bits: np.ndarray, length: int) -> str:
        """Transform the bytes into string, example 65 -> 'A'"""
        bits_num = np.packbits(bits)
        bits_string = b""
        for i in range(length // 8):
            bits_string += struct.pack(">B", bits_num[i])
        return bits_string.decode("utf-8")

    def encode(
        self, carrier: collections.OrderedDict, payload: str
    ) -> tuple[collections.OrderedDict, int]:
        """Encodes payload string into an NN model by LSB.

        Args:
            carrier: The carrier NN model, read with `io.nnmodel.read()`.
            payload: The payload string.

        Returns:
            The encoded NN model, write with `io.nnmodel.write()`.
            Length of payload in bits, you will need this when decoding.
        """
        secret_bits = self.payload_in_bytes(payload)

        length = len(secret_bits)

        # This dict holds the original weights for the selected layers
        original_weights_dict: dict = {}

        for te in carrier:

            # if re.match(".*conv.*", te):

            original_weights_dict[te] = deepcopy(
                carrier[te].clone().detach().cpu().numpy()
            )

        layer_name = list(original_weights_dict.keys())
        layer_name_num = {}
        pre_layer_name = {}
        layer_shape = {}
        for name in layer_name:
            layer_name_num[name] = np.prod(original_weights_dict[name].shape)

        for i in range(len(layer_name)):
            name = layer_name[i]
            if i == 0:
                pre_layer_name[name] = 0
            else:
                pre_layer_name[name] = (
                    layer_name_num[layer_name[i - 1]]
                    + pre_layer_name[layer_name[i - 1]]
                )

        carrier_data = []
        for name in layer_name:
            layer_shape[name] = original_weights_dict[name].shape
            carrier_data.extend(original_weights_dict[name].reshape(-1))

        for i in range(0, length, self.bit_use):
            _from_index = i
            _to_index = _from_index + self.bit_use
            bits_to_hide = secret_bits[_from_index:_to_index]
            bits_to_hide = list(map(bool, bits_to_hide))  # type: ignore

            j = math.ceil(i / self.bit_use)
            x = FloatBinary(carrier_data[j])
            fraction_modified = list(x.fraction)
            if len(bits_to_hide) > 0:
                fraction_modified[-self.bit_use :] = bits_to_hide

            x_modified = x.modify_clone(fraction=fraction_modified)
            carrier_data[j] = x_modified.v

        outdata = {}
        for name in layer_name:

            temp = np.array(
                carrier_data[
                    int(pre_layer_name[name]) : int(pre_layer_name[name])
                    + int(layer_name_num[name])
                ]
            )
            outdata[name] = temp.reshape(layer_shape[name])

        for name in layer_name:

            carrier.pop(name)

            carrier[name] = torch.from_numpy(outdata[name])
            # carrier[name] = torch.nn.parameter.Parameter(

            #     torch.from_numpy(outdata[name]), requires_grad= False
            # )

        return carrier, length

    def decode(self, _):
        raise NotImplementedError("This codec does not support decoding without length")

    def decodewithlength(self, carrier: collections.OrderedDict, length: int) -> str:
        """Try to decode payload from an NN model by LSB.

        Args:
            carrier: the carrier NN model,read with `io.nnmodel.read()`.
            length: the length of payload.

        Returns:
            str: The payload string.
        """
        hidden_data: List[bool] = []
        original_weights_dict: dict = {}
        for te in carrier:

            # if re.match(".*conv.*", te):
            original_weights_dict[te] = deepcopy(
                carrier[te].clone().detach().cpu().numpy()
            )

        layer_name = list(original_weights_dict.keys())
        carrier_data = []
        for name in layer_name:
            carrier_data.extend(original_weights_dict[name].reshape(-1))

        for i in range(0, length, self.bit_use):

            j = math.ceil(i / self.bit_use)

            x = FloatBinary(carrier_data[j])
            hidden_bits = x.fraction[-self.bit_use :]
            hidden_data.extend(hidden_bits)
        hidden_data = list(np.array(hidden_data).astype(int))
        recovered_message = self.reconstruct_bytes(hidden_data, length)  # type: ignore

        return recovered_message

__init__(num_bits=8)

To initialize the class and specify the number of bits to use.

Parameters:

Name Type Description Default
num_bits int

The number of bits to hide secret information.

8
Source code in stegobox/codec/hide_str_in_nn_model/lsb_hide_str_in_nnmodel.py
def __init__(self, num_bits: int = 8) -> None:
    """To initialize the class and specify the number of bits to use.

    Args:
        num_bits: The number of bits to hide secret information.
    """
    super().__init__()
    self.bit_use = num_bits

payload_in_bytes(payload)

Transform the string into bytes, example 'A' -> 65

Source code in stegobox/codec/hide_str_in_nn_model/lsb_hide_str_in_nnmodel.py
def payload_in_bytes(self, payload: str) -> list[np.uint8]:
    """Transform the string into bytes, example 'A' -> 65"""
    payloadb = payload.encode("utf-8")
    payloadsq = np.array([n for n in payloadb], dtype=np.uint8)
    return list(np.unpackbits(payloadsq))

reconstruct_bytes(bits, length)

Transform the bytes into string, example 65 -> 'A'

Source code in stegobox/codec/hide_str_in_nn_model/lsb_hide_str_in_nnmodel.py
def reconstruct_bytes(self, bits: np.ndarray, length: int) -> str:
    """Transform the bytes into string, example 65 -> 'A'"""
    bits_num = np.packbits(bits)
    bits_string = b""
    for i in range(length // 8):
        bits_string += struct.pack(">B", bits_num[i])
    return bits_string.decode("utf-8")

encode(carrier, payload)

Encodes payload string into an NN model by LSB.

Parameters:

Name Type Description Default
carrier OrderedDict

The carrier NN model, read with io.nnmodel.read().

required
payload str

The payload string.

required

Returns:

Type Description
OrderedDict

The encoded NN model, write with io.nnmodel.write().

int

Length of payload in bits, you will need this when decoding.

Source code in stegobox/codec/hide_str_in_nn_model/lsb_hide_str_in_nnmodel.py
def encode(
    self, carrier: collections.OrderedDict, payload: str
) -> tuple[collections.OrderedDict, int]:
    """Encodes payload string into an NN model by LSB.

    Args:
        carrier: The carrier NN model, read with `io.nnmodel.read()`.
        payload: The payload string.

    Returns:
        The encoded NN model, write with `io.nnmodel.write()`.
        Length of payload in bits, you will need this when decoding.
    """
    secret_bits = self.payload_in_bytes(payload)

    length = len(secret_bits)

    # This dict holds the original weights for the selected layers
    original_weights_dict: dict = {}

    for te in carrier:

        # if re.match(".*conv.*", te):

        original_weights_dict[te] = deepcopy(
            carrier[te].clone().detach().cpu().numpy()
        )

    layer_name = list(original_weights_dict.keys())
    layer_name_num = {}
    pre_layer_name = {}
    layer_shape = {}
    for name in layer_name:
        layer_name_num[name] = np.prod(original_weights_dict[name].shape)

    for i in range(len(layer_name)):
        name = layer_name[i]
        if i == 0:
            pre_layer_name[name] = 0
        else:
            pre_layer_name[name] = (
                layer_name_num[layer_name[i - 1]]
                + pre_layer_name[layer_name[i - 1]]
            )

    carrier_data = []
    for name in layer_name:
        layer_shape[name] = original_weights_dict[name].shape
        carrier_data.extend(original_weights_dict[name].reshape(-1))

    for i in range(0, length, self.bit_use):
        _from_index = i
        _to_index = _from_index + self.bit_use
        bits_to_hide = secret_bits[_from_index:_to_index]
        bits_to_hide = list(map(bool, bits_to_hide))  # type: ignore

        j = math.ceil(i / self.bit_use)
        x = FloatBinary(carrier_data[j])
        fraction_modified = list(x.fraction)
        if len(bits_to_hide) > 0:
            fraction_modified[-self.bit_use :] = bits_to_hide

        x_modified = x.modify_clone(fraction=fraction_modified)
        carrier_data[j] = x_modified.v

    outdata = {}
    for name in layer_name:

        temp = np.array(
            carrier_data[
                int(pre_layer_name[name]) : int(pre_layer_name[name])
                + int(layer_name_num[name])
            ]
        )
        outdata[name] = temp.reshape(layer_shape[name])

    for name in layer_name:

        carrier.pop(name)

        carrier[name] = torch.from_numpy(outdata[name])
        # carrier[name] = torch.nn.parameter.Parameter(

        #     torch.from_numpy(outdata[name]), requires_grad= False
        # )

    return carrier, length

decodewithlength(carrier, length)

Try to decode payload from an NN model by LSB.

Parameters:

Name Type Description Default
carrier OrderedDict

the carrier NN model,read with io.nnmodel.read().

required
length int

the length of payload.

required

Returns:

Name Type Description
str str

The payload string.

Source code in stegobox/codec/hide_str_in_nn_model/lsb_hide_str_in_nnmodel.py
def decodewithlength(self, carrier: collections.OrderedDict, length: int) -> str:
    """Try to decode payload from an NN model by LSB.

    Args:
        carrier: the carrier NN model,read with `io.nnmodel.read()`.
        length: the length of payload.

    Returns:
        str: The payload string.
    """
    hidden_data: List[bool] = []
    original_weights_dict: dict = {}
    for te in carrier:

        # if re.match(".*conv.*", te):
        original_weights_dict[te] = deepcopy(
            carrier[te].clone().detach().cpu().numpy()
        )

    layer_name = list(original_weights_dict.keys())
    carrier_data = []
    for name in layer_name:
        carrier_data.extend(original_weights_dict[name].reshape(-1))

    for i in range(0, length, self.bit_use):

        j = math.ceil(i / self.bit_use)

        x = FloatBinary(carrier_data[j])
        hidden_bits = x.fraction[-self.bit_use :]
        hidden_data.extend(hidden_bits)
    hidden_data = list(np.array(hidden_data).astype(int))
    recovered_message = self.reconstruct_bytes(hidden_data, length)  # type: ignore

    return recovered_message