Secure File Transfer Across Monitored Network#
在特殊情况下, 你可能必须要在不可信的机器上需要传送文件. 而这个机器所在的网络可能会有监控软件能对你的网络流量抓包分析, 从而看到内容. 又或者通过防火墙限制了一些传输文件的网络服务. 那这个情况下要怎么做才安全呢?
我的解决案有以下几个要点:
利用 AWS S3 作为中转媒介. 它支持 encryption at transit and at rest. 在网络传输过程和存储中都是加密的.
使用 windtalker Python 库做客户端加密, 保证文件在发送之前就已经被加密了.
密钥在命令行输入, 不留记录. 本地机器的网络监控只知道你把文件上传到 S3 了, 但并不知道内容是什么, 截取了内容也无法解密. AWS S3 本身就算拿到数据也不知道压缩包的内容是什么.
Secure file transfer app Python library app.py
1# -*- coding: utf-8 -*-
2
3"""
4This is an importable library can securely transfer file / folder across
5monitored network. It uses AWS S3 to store the encrypted data and use symmetric
6encryption algorithm to encrypt / decrypt the data.
7
8Requirements:
9
10- Python >= 3.7
11- Dependencies::
12
13 boto3
14 boto_session_manager>=1.7.2,<2.0.0
15 s3pathlib>=2.1.2,<3.0.0
16 windtalker>=1.0.1,<2.0.0
17"""
18
19import typing as T
20import shutil
21import dataclasses
22
23from pathlib_mate import Path, T_PATH_ARG
24from boto_session_manager import BotoSesManager
25from s3pathlib import S3Path
26from windtalker.api import SymmetricCipher
27
28
29@dataclasses.dataclass
30class App:
31 """
32 Secure file transfer app.
33 """
34
35 bsm: BotoSesManager = dataclasses.field()
36 bucket: str = dataclasses.field()
37 folder: str = dataclasses.field()
38 _dir_tmp: Path = dataclasses.field()
39 _cipher: SymmetricCipher = dataclasses.field()
40 _s3dir: S3Path = dataclasses.field()
41
42 @classmethod
43 def new(
44 cls,
45 bsm: BotoSesManager,
46 bucket: str,
47 folder: str,
48 password: T.Optional[str] = None,
49 dir_tmp: T.Optional[T_PATH_ARG] = None,
50 ):
51 """
52 Create a new instance of App.
53
54 :param bsm: the boto3 session manager you want to use
55 :param bucket: which s3 bucket to store the encrypted data
56 :param folder: which folder in the s3 bucket to store the encrypted data
57 :param password: the password you want to use to encrypt the data
58 :param dir_tmp: the temp dir you want to use to store the temporary data,
59 by default, it is ``${HOME}/tmp``.
60 """
61 if dir_tmp is None:
62 dir_tmp = Path.home().joinpath("tmp")
63 dir_tmp.mkdir_if_not_exists()
64 else:
65 dir_tmp = Path(dir_tmp)
66 return cls(
67 bsm=bsm,
68 bucket=bucket,
69 folder=folder,
70 _cipher=SymmetricCipher(password=password),
71 _s3dir=S3Path(bucket, folder).to_dir(),
72 _dir_tmp=dir_tmp,
73 )
74
75 def upload(
76 self,
77 archive_name: str,
78 path: T_PATH_ARG,
79 overwrite: bool = True,
80 ) -> S3Path:
81 """
82
83 :param archive_name: a unique name for the archive.
84 :param path: which file / dir you want to transfer.
85 :param overwrite: if True, overwrite existing file in AWS S3.
86 """
87 path = Path(path).absolute()
88 s3path = self._s3dir.joinpath(archive_name, "encrypted")
89
90 # --- file logic
91 if path.is_file():
92 if overwrite is False: # pragma: no cover
93 if s3path.exists(bsm=self.bsm) is True:
94 raise FileExistsError(f"{s3path.uri} already exists.")
95
96 s3path.write_bytes(
97 data=self._cipher.encrypt_binary(path.read_bytes()),
98 metadata={
99 "type": "file",
100 "filename": path.basename,
101 },
102 bsm=self.bsm,
103 )
104 return s3path
105
106 # --- dir logic
107 elif path.is_dir():
108 dir_tmp_archive = self._dir_tmp.joinpath(archive_name)
109 try:
110 dir_tmp_archive.mkdir_if_not_exists()
111
112 path_output = dir_tmp_archive.joinpath(path.basename)
113 self._cipher.encrypt_dir(
114 path=path,
115 output_path=path_output,
116 overwrite=overwrite,
117 )
118 path_output_zip = dir_tmp_archive.joinpath(path.basename + ".zip")
119 path_output.make_zip_archive(
120 dst=path_output_zip,
121 overwrite=overwrite,
122 include_dir=True,
123 )
124 s3path.upload_file(
125 path_output_zip,
126 extra_args=dict(
127 Metadata={
128 "type": "dir",
129 "dirname": path.basename,
130 }
131 ),
132 overwrite=overwrite,
133 )
134 except Exception as e:
135 shutil.rmtree(dir_tmp_archive.abspath)
136 raise e
137 return s3path
138 else: # pragma: no cover
139 raise NotImplementedError
140
141 def download(
142 self,
143 archive_name: str,
144 path: T.Optional[T_PATH_ARG] = None,
145 overwrite: bool = True,
146 ):
147 """
148
149 :param archive_name: a unique name for the archive.
150 :param path: where you want to store the downloaded file / dir.
151 if None, download to current working directory.
152 :param overwrite: if True, overwrite existing file / dir.
153 """
154 s3path = self._s3dir.joinpath(archive_name, "encrypted")
155
156 # --- file logic
157 if s3path.metadata["type"] == "file":
158 if path is None:
159 path = Path.cwd().joinpath(s3path.metadata["filename"])
160 else:
161 path = Path(path).absolute()
162 if path.exists() and overwrite is False:
163 raise FileExistsError(f"{path} already exists.")
164 path.write_bytes(self._cipher.decrypt_binary(s3path.read_bytes()))
165
166 # --- dir logic
167 elif s3path.metadata["type"] == "dir":
168 dir_tmp_archive = self._dir_tmp.joinpath(archive_name)
169 try:
170 dir_tmp_archive.mkdir_if_not_exists()
171 path_tmp_zip = dir_tmp_archive.joinpath(
172 s3path.metadata["dirname"] + ".zip"
173 )
174 self.bsm.s3_client.download_file(
175 Bucket=s3path.bucket,
176 Key=s3path.key,
177 Filename=path_tmp_zip.abspath,
178 )
179 shutil.unpack_archive(path_tmp_zip.abspath, dir_tmp_archive.abspath)
180 dir_extracted = dir_tmp_archive.joinpath(s3path.metadata["dirname"])
181
182 if path is None:
183 dir_here = Path.cwd()
184 self._cipher.decrypt_dir(
185 path=dir_extracted,
186 output_path=dir_here.joinpath(s3path.metadata["dirname"]),
187 overwrite=overwrite,
188 )
189 else:
190 self._cipher.decrypt_dir(
191 path=dir_extracted,
192 output_path=path,
193 overwrite=overwrite,
194 )
195
196 except Exception as e:
197 shutil.rmtree(dir_tmp_archive.abspath)
198 raise e
Example usage of app.py
1# -*- coding: utf-8 -*-
2
3from pathlib_mate import Path
4from app import App, BotoSesManager
5
6dir_here = Path.dir_here(__file__)
7
8app = App.new(
9 bsm=BotoSesManager(profile_name="bmt_app_dev_us_east_1"),
10 bucket="bmt-app-dev-us-east-1-data",
11 folder="projects/secure-file-transfer-across-monitored-network",
12 password="mypassword",
13)
14
15# ------------------------------------------------------------------------------
16# File
17# ------------------------------------------------------------------------------
18# --- upload
19# s3path = app.upload(archive_name="archive_test_file", path="test-file.txt")
20# print(s3path.console_url)
21
22# --- download
23# app.download(archive_name="archive_test_file", path="tmp/downloaded-test-file.txt")
24
25# ------------------------------------------------------------------------------
26# Folder
27# ------------------------------------------------------------------------------
28# --- upload
29# s3path = app.upload(archive_name="archive_test_dir", path=dir_here.joinpath("test-dir"))
30# print(s3path.console_url)
31
32# --- download
33# app.download(archive_name="archive_test_dir", path=dir_here.joinpath("tmp", "downloaded-test-dir"))