SFTP encrypts everything being transferred over the SSH data stream; from the authentication of the users to the actual files being transferred. If any part of the data is intercepted, it will be unreadable because of the encryption. Hence, an extra encryption layer is not actually necessary.
However, if it’s required, it is possible to add an extra encryption layer, so files will be encrypted
before it is passed over the SFTP protocol. But that will mean:
- Applications in the destination server that will use the files must be modified to be able to read the encrypted files, i.e. instead of simply pick up a transferred file and use it immediately, the application will need to pick up the file, decrypt, and use it.
- There’s an extra overhead for encrypting and decrypting the files.
Extra encryption layer that is supported by EasiShare’s SFTP automation module is symmetric encryption (tag 9, reference: https://datatracker.ietf.org/doc/html/rfc4880#section-5.7) with AES256 algorithm, and the encryption key is SHA256 key derivation of a passphrase.
The extra encryption layer can be set at task level. So, for example, we can define:
- Task 1: Transfer files from A to B, use extra encryption layer.
- Task 2: Transfer files from X to Y, no extra encryption layer.
If an extra encryption layer is used, the SFTP automation module will:
- Pull unencrypted data from the source server (over encrypted SSH data stream) to
EasiShare App Server. - Apply the encryption (this process will incur an extra overhead).
- Send the encrypted files (over encrypted SSH data stream) to the destination server.
In the destination server, the application that will use the files will need to be able to
decrypt the files. Here’s an example of the decryption implementation, in Python:
import binascii
import hashlib
import sys
from Cryptodome.Cipher import AES
def print_bytes(name, data):
print("%s(len: %d): %s" % (name, len(data), str(binascii.hexlify(data))))
def main(filename):
# Generate key material from the passphrase.
passphrase = b"secret passphrase"
m = hashlib.sha256()
m.update(passphrase)
key = m.digest()
# Get file contents.
with open(filename, "rb") as f:
contents = f.read()
print_bytes("contents", contents)
# Constants
header_len = 9 # including the 1-octet type-19 version identifier
block_size = 16 # alogorithm details should normally be extracted from the
header
segment_size = block_size * 8
iv_len = block_size
iv_tag_len = 2
mdc_len = 22
# Decrypting the data, adhere to
# https://datatracker.ietf.org/doc/html/rfc4880#section-5.13
cipher = AES.new(key, AES.MODE_CFB, iv=b"\x00" * block_size,
segment_size=segment_size)
offset = header_len
decrypted_iv = cipher.decrypt(contents[offset:offset+block_size])
print_bytes("decrypted_iv", decrypted_iv)
decrypted_data = bytearray()
offset += block_size
first_block = cipher.decrypt(contents[offset:offset+block_size])
print_bytes("first_block", first_block)
offset += block_size
if first_block[:2] != decrypted_iv[-2:]:
print("IV check failed")
sys.exit(1)
decrypted_data.extend(first_block[2:])
print_bytes("decrypted_data (first block)", decrypted_data)
padding = block_size - (len(contents)-offset) % block_size
contents += b"\x00" * padding
decrypted_data.extend(cipher.decrypt(contents[offset:]))
decrypted_data = decrypted_data[:-(mdc_len+padding)]
print_bytes("decrypted_data", decrypted_data)
# Extract filename length so we can find the plaintext offset.
# see https://datatracker.ietf.org/doc/html/rfc4880#section-5.9
filename_len = decrypted_data[3]
plaintext_offset = (
2 # header
+ 1 # file type
+ 1 # filename length
+ filename_len # filename contents
+ 4 # timestamp
)
plaintext = decrypted_data[plaintext_offset:]
print_bytes("plaintext", plaintext)
print(plaintext.decode())
if __name__ == "__main__":
if (len(sys.argv) > 1):
main(sys.argv[1])
else:
print("first argument must be file to decrypt")
sys.exit(1)
Comments