key.py 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059
  1. import binascii
  2. import hmac
  3. import os
  4. import textwrap
  5. from binascii import a2b_base64, b2a_base64, hexlify
  6. from hashlib import sha256, pbkdf2_hmac
  7. from typing import Literal, Callable, ClassVar
  8. from ..logger import create_logger
  9. logger = create_logger()
  10. import argon2.low_level
  11. from ..constants import * # NOQA
  12. from ..helpers import StableDict
  13. from ..helpers import Error, IntegrityError
  14. from ..helpers import get_keys_dir, get_security_dir
  15. from ..helpers import get_limited_unpacker
  16. from ..helpers import bin_to_hex
  17. from ..helpers.passphrase import Passphrase, PasswordRetriesExceeded, PassphraseWrong
  18. from ..helpers import msgpack
  19. from ..helpers import workarounds
  20. from ..item import Key, EncryptedKey, want_bytes
  21. from ..manifest import Manifest
  22. from ..platform import SaveFile
  23. from ..repoobj import RepoObj
  24. from .low_level import AES, bytes_to_int, num_cipher_blocks, hmac_sha256, blake2b_256, hkdf_hmac_sha512
  25. from .low_level import AES256_CTR_HMAC_SHA256, AES256_CTR_BLAKE2b, AES256_OCB, CHACHA20_POLY1305
  26. from . import low_level
  27. # workaround for lost passphrase or key in "authenticated" or "authenticated-blake2" mode
  28. AUTHENTICATED_NO_KEY = "authenticated_no_key" in workarounds
  29. class UnsupportedPayloadError(Error):
  30. """Unsupported payload type {}. A newer version is required to access this repository."""
  31. class UnsupportedManifestError(Error):
  32. """Unsupported manifest envelope. A newer version is required to access this repository."""
  33. class KeyfileNotFoundError(Error):
  34. """No key file for repository {} found in {}."""
  35. class KeyfileInvalidError(Error):
  36. """Invalid key file for repository {} found in {}."""
  37. class KeyfileMismatchError(Error):
  38. """Mismatch between repository {} and key file {}."""
  39. class RepoKeyNotFoundError(Error):
  40. """No key entry found in the config of repository {}."""
  41. class UnsupportedKeyFormatError(Error):
  42. """Your borg key is stored in an unsupported format. Try using a newer version of borg."""
  43. class TAMRequiredError(IntegrityError):
  44. __doc__ = textwrap.dedent(
  45. """
  46. Manifest is unauthenticated, but it is required for this repository. Is somebody attacking you?
  47. """
  48. ).strip()
  49. traceback = False
  50. class TAMInvalid(IntegrityError):
  51. __doc__ = IntegrityError.__doc__
  52. traceback = False
  53. def __init__(self):
  54. # Error message becomes: "Data integrity error: Manifest authentication did not verify"
  55. super().__init__("Manifest authentication did not verify")
  56. class TAMUnsupportedSuiteError(IntegrityError):
  57. """Could not verify manifest: Unsupported suite {!r}; a newer version is needed."""
  58. traceback = False
  59. def key_creator(repository, args, *, other_key=None):
  60. for key in AVAILABLE_KEY_TYPES:
  61. if key.ARG_NAME == args.encryption:
  62. assert key.ARG_NAME is not None
  63. return key.create(repository, args, other_key=other_key)
  64. else:
  65. raise ValueError('Invalid encryption mode "%s"' % args.encryption)
  66. def key_argument_names():
  67. return [key.ARG_NAME for key in AVAILABLE_KEY_TYPES if key.ARG_NAME]
  68. def identify_key(manifest_data):
  69. key_type = manifest_data[0]
  70. if key_type == KeyType.PASSPHRASE: # legacy, see comment in KeyType class.
  71. return RepoKey
  72. for key in LEGACY_KEY_TYPES + AVAILABLE_KEY_TYPES:
  73. if key.TYPE == key_type:
  74. return key
  75. else:
  76. raise UnsupportedPayloadError(key_type)
  77. def key_factory(repository, manifest_chunk, *, ro_cls=RepoObj):
  78. manifest_data = ro_cls.extract_crypted_data(manifest_chunk)
  79. assert manifest_data, "manifest data must not be zero bytes long"
  80. return identify_key(manifest_data).detect(repository, manifest_data)
  81. def tam_required_file(repository):
  82. security_dir = get_security_dir(bin_to_hex(repository.id), legacy=(repository.version == 1))
  83. return os.path.join(security_dir, "tam_required")
  84. def tam_required(repository):
  85. file = tam_required_file(repository)
  86. return os.path.isfile(file)
  87. def uses_same_chunker_secret(other_key, key):
  88. """is the chunker secret the same?"""
  89. # avoid breaking the deduplication by a different chunker secret
  90. same_chunker_secret = other_key.chunk_seed == key.chunk_seed
  91. return same_chunker_secret
  92. def uses_same_id_hash(other_key, key):
  93. """other_key -> key upgrade: is the id hash the same?"""
  94. # avoid breaking the deduplication by changing the id hash
  95. old_sha256_ids = (PlaintextKey,)
  96. new_sha256_ids = (PlaintextKey,)
  97. old_hmac_sha256_ids = (RepoKey, KeyfileKey, AuthenticatedKey)
  98. new_hmac_sha256_ids = (AESOCBRepoKey, AESOCBKeyfileKey, CHPORepoKey, CHPOKeyfileKey, AuthenticatedKey)
  99. old_blake2_ids = (Blake2RepoKey, Blake2KeyfileKey, Blake2AuthenticatedKey)
  100. new_blake2_ids = (
  101. Blake2AESOCBRepoKey,
  102. Blake2AESOCBKeyfileKey,
  103. Blake2CHPORepoKey,
  104. Blake2CHPOKeyfileKey,
  105. Blake2AuthenticatedKey,
  106. )
  107. same_ids = (
  108. isinstance(other_key, old_hmac_sha256_ids + new_hmac_sha256_ids)
  109. and isinstance(key, new_hmac_sha256_ids)
  110. or isinstance(other_key, old_blake2_ids + new_blake2_ids)
  111. and isinstance(key, new_blake2_ids)
  112. or isinstance(other_key, old_sha256_ids + new_sha256_ids)
  113. and isinstance(key, new_sha256_ids)
  114. )
  115. return same_ids
  116. class KeyBase:
  117. # Numeric key type ID, must fit in one byte.
  118. TYPE: int = None # override in subclasses
  119. # set of key type IDs the class can handle as input
  120. TYPES_ACCEPTABLE: set[int] = None # override in subclasses
  121. # Human-readable name
  122. NAME = "UNDEFINED"
  123. # Name used in command line / API (e.g. borg init --encryption=...)
  124. ARG_NAME = "UNDEFINED"
  125. # Storage type (no key blob storage / keyfile / repo)
  126. STORAGE: ClassVar[str] = KeyBlobStorage.NO_STORAGE
  127. # Seed for the buzhash chunker (borg.algorithms.chunker.Chunker)
  128. # type is int
  129. chunk_seed: int = None
  130. # Whether this *particular instance* is encrypted from a practical point of view,
  131. # i.e. when it's using encryption with a empty passphrase, then
  132. # that may be *technically* called encryption, but for all intents and purposes
  133. # that's as good as not encrypting in the first place, and this member should be False.
  134. #
  135. # The empty passphrase is also special because Borg tries it first when no passphrase
  136. # was supplied, and if an empty passphrase works, then Borg won't ask for one.
  137. logically_encrypted = False
  138. def __init__(self, repository):
  139. self.TYPE_STR = bytes([self.TYPE])
  140. self.repository = repository
  141. self.target = None # key location file path / repo obj
  142. self.tam_required = True
  143. self.copy_crypt_key = False
  144. def id_hash(self, data):
  145. """Return HMAC hash using the "id" HMAC key"""
  146. raise NotImplementedError
  147. def encrypt(self, id, data):
  148. pass
  149. def decrypt(self, id, data):
  150. pass
  151. def assert_id(self, id, data):
  152. if id and id != Manifest.MANIFEST_ID:
  153. id_computed = self.id_hash(data)
  154. if not hmac.compare_digest(id_computed, id):
  155. raise IntegrityError("Chunk %s: id verification failed" % bin_to_hex(id))
  156. def assert_type(self, type_byte, id=None):
  157. if type_byte not in self.TYPES_ACCEPTABLE:
  158. id_str = bin_to_hex(id) if id is not None else "(unknown)"
  159. raise IntegrityError(f"Chunk {id_str}: Invalid encryption envelope")
  160. def _tam_key(self, salt, context):
  161. return hkdf_hmac_sha512(
  162. ikm=self.id_key + self.crypt_key,
  163. salt=salt,
  164. info=b"borg-metadata-authentication-" + context,
  165. output_length=64,
  166. )
  167. def pack_and_authenticate_metadata(self, metadata_dict, context=b"manifest"):
  168. metadata_dict = StableDict(metadata_dict)
  169. tam = metadata_dict["tam"] = StableDict({"type": "HKDF_HMAC_SHA512", "hmac": bytes(64), "salt": os.urandom(64)})
  170. packed = msgpack.packb(metadata_dict)
  171. tam_key = self._tam_key(tam["salt"], context)
  172. tam["hmac"] = hmac.digest(tam_key, packed, "sha512")
  173. return msgpack.packb(metadata_dict)
  174. def unpack_and_verify_manifest(self, data, force_tam_not_required=False):
  175. """Unpack msgpacked *data* and return (object, did_verify)."""
  176. if data.startswith(b"\xc1" * 4):
  177. # This is a manifest from the future, we can't read it.
  178. raise UnsupportedManifestError()
  179. tam_required = self.tam_required
  180. if force_tam_not_required and tam_required:
  181. logger.warning("Manifest authentication DISABLED.")
  182. tam_required = False
  183. data = bytearray(data)
  184. unpacker = get_limited_unpacker("manifest")
  185. unpacker.feed(data)
  186. unpacked = unpacker.unpack()
  187. if AUTHENTICATED_NO_KEY:
  188. return unpacked, True # True is a lie.
  189. if "tam" not in unpacked:
  190. if tam_required:
  191. raise TAMRequiredError(self.repository._location.canonical_path())
  192. else:
  193. logger.debug("TAM not found and not required")
  194. return unpacked, False
  195. tam = unpacked.pop("tam", None)
  196. if not isinstance(tam, dict):
  197. raise TAMInvalid()
  198. tam_type = tam.get("type", "<none>")
  199. if tam_type != "HKDF_HMAC_SHA512":
  200. if tam_required:
  201. raise TAMUnsupportedSuiteError(repr(tam_type))
  202. else:
  203. logger.debug("Ignoring TAM made with unsupported suite, since TAM is not required: %r", tam_type)
  204. return unpacked, False
  205. tam_hmac = tam.get("hmac")
  206. tam_salt = tam.get("salt")
  207. if not isinstance(tam_salt, (bytes, str)) or not isinstance(tam_hmac, (bytes, str)):
  208. raise TAMInvalid()
  209. tam_hmac = want_bytes(tam_hmac) # legacy
  210. tam_salt = want_bytes(tam_salt) # legacy
  211. offset = data.index(tam_hmac)
  212. data[offset : offset + 64] = bytes(64)
  213. tam_key = self._tam_key(tam_salt, context=b"manifest")
  214. calculated_hmac = hmac.digest(tam_key, data, "sha512")
  215. if not hmac.compare_digest(calculated_hmac, tam_hmac):
  216. raise TAMInvalid()
  217. logger.debug("TAM-verified manifest")
  218. return unpacked, True
  219. class PlaintextKey(KeyBase):
  220. TYPE = KeyType.PLAINTEXT
  221. TYPES_ACCEPTABLE = {TYPE}
  222. NAME = "plaintext"
  223. ARG_NAME = "none"
  224. chunk_seed = 0
  225. logically_encrypted = False
  226. def __init__(self, repository):
  227. super().__init__(repository)
  228. self.tam_required = False
  229. @classmethod
  230. def create(cls, repository, args, **kw):
  231. logger.info('Encryption NOT enabled.\nUse the "--encryption=repokey|keyfile" to enable encryption.')
  232. return cls(repository)
  233. @classmethod
  234. def detect(cls, repository, manifest_data):
  235. return cls(repository)
  236. def id_hash(self, data):
  237. return sha256(data).digest()
  238. def encrypt(self, id, data):
  239. return b"".join([self.TYPE_STR, data])
  240. def decrypt(self, id, data):
  241. self.assert_type(data[0], id)
  242. return memoryview(data)[1:]
  243. def _tam_key(self, salt, context):
  244. return salt + context
  245. def random_blake2b_256_key():
  246. # This might look a bit curious, but is the same construction used in the keyed mode of BLAKE2b.
  247. # Why limit the key to 64 bytes and pad it with 64 nulls nonetheless? The answer is that BLAKE2b
  248. # has a 128 byte block size, but only 64 bytes of internal state (this is also referred to as a
  249. # "local wide pipe" design, because the compression function transforms (block, state) => state,
  250. # and len(block) >= len(state), hence wide.)
  251. # In other words, a key longer than 64 bytes would have simply no advantage, since the function
  252. # has no way of propagating more than 64 bytes of entropy internally.
  253. # It's padded to a full block so that the key is never buffered internally by blake2b_update, ie.
  254. # it remains in a single memory location that can be tracked and could be erased securely, if we
  255. # wanted to.
  256. return os.urandom(64) + bytes(64)
  257. class ID_BLAKE2b_256:
  258. """
  259. Key mix-in class for using BLAKE2b-256 for the id key.
  260. The id_key length must be 32 bytes.
  261. """
  262. def id_hash(self, data):
  263. return blake2b_256(self.id_key, data)
  264. def init_from_random_data(self):
  265. super().init_from_random_data()
  266. enc_key = os.urandom(32)
  267. enc_hmac_key = random_blake2b_256_key()
  268. self.crypt_key = enc_key + enc_hmac_key
  269. self.id_key = random_blake2b_256_key()
  270. class ID_HMAC_SHA_256:
  271. """
  272. Key mix-in class for using HMAC-SHA-256 for the id key.
  273. The id_key length must be 32 bytes.
  274. """
  275. def id_hash(self, data):
  276. return hmac_sha256(self.id_key, data)
  277. class AESKeyBase(KeyBase):
  278. """
  279. Chunks are encrypted using 256bit AES in Counter Mode (CTR)
  280. Payload layout: TYPE(1) + HMAC(32) + NONCE(8) + CIPHERTEXT
  281. To reduce payload size only 8 bytes of the 16 bytes nonce is saved
  282. in the payload, the first 8 bytes are always zeros. This does not
  283. affect security but limits the maximum repository capacity to
  284. only 295 exabytes!
  285. """
  286. PAYLOAD_OVERHEAD = 1 + 32 + 8 # TYPE + HMAC + NONCE
  287. CIPHERSUITE: Callable = None # override in derived class
  288. logically_encrypted = True
  289. def encrypt(self, id, data):
  290. # legacy, this is only used by the tests.
  291. next_iv = self.cipher.next_iv()
  292. return self.cipher.encrypt(data, header=self.TYPE_STR, iv=next_iv)
  293. def decrypt(self, id, data):
  294. self.assert_type(data[0], id)
  295. try:
  296. return self.cipher.decrypt(data)
  297. except IntegrityError as e:
  298. raise IntegrityError(f"Chunk {bin_to_hex(id)}: Could not decrypt [{str(e)}]")
  299. def init_from_given_data(self, *, crypt_key, id_key, chunk_seed):
  300. assert len(crypt_key) in (32 + 32, 32 + 128)
  301. assert len(id_key) in (32, 128)
  302. assert isinstance(chunk_seed, int)
  303. self.crypt_key = crypt_key
  304. self.id_key = id_key
  305. self.chunk_seed = chunk_seed
  306. def init_from_random_data(self):
  307. data = os.urandom(100)
  308. chunk_seed = bytes_to_int(data[96:100])
  309. # Convert to signed int32
  310. if chunk_seed & 0x80000000:
  311. chunk_seed = chunk_seed - 0xFFFFFFFF - 1
  312. self.init_from_given_data(crypt_key=data[0:64], id_key=data[64:96], chunk_seed=chunk_seed)
  313. def init_ciphers(self, manifest_data=None):
  314. enc_key, enc_hmac_key = self.crypt_key[0:32], self.crypt_key[32:]
  315. self.cipher = self.CIPHERSUITE(mac_key=enc_hmac_key, enc_key=enc_key, header_len=1, aad_offset=1)
  316. if manifest_data is None:
  317. nonce = 0
  318. else:
  319. self.assert_type(manifest_data[0])
  320. # manifest_blocks is a safe upper bound on the amount of cipher blocks needed
  321. # to encrypt the manifest. depending on the ciphersuite and overhead, it might
  322. # be a bit too high, but that does not matter.
  323. manifest_blocks = num_cipher_blocks(len(manifest_data))
  324. nonce = self.cipher.extract_iv(manifest_data) + manifest_blocks
  325. self.cipher.set_iv(nonce)
  326. class FlexiKey:
  327. FILE_ID = "BORG_KEY"
  328. STORAGE: ClassVar[str] = KeyBlobStorage.NO_STORAGE # override in subclass
  329. @classmethod
  330. def detect(cls, repository, manifest_data):
  331. key = cls(repository)
  332. target = key.find_key()
  333. prompt = "Enter passphrase for key %s: " % target
  334. passphrase = Passphrase.env_passphrase()
  335. if passphrase is None:
  336. passphrase = Passphrase()
  337. if not key.load(target, passphrase):
  338. for retry in range(0, 3):
  339. passphrase = Passphrase.getpass(prompt)
  340. if key.load(target, passphrase):
  341. break
  342. else:
  343. raise PasswordRetriesExceeded
  344. else:
  345. if not key.load(target, passphrase):
  346. raise PassphraseWrong
  347. key.init_ciphers(manifest_data)
  348. key._passphrase = passphrase
  349. return key
  350. def _load(self, key_data, passphrase):
  351. cdata = a2b_base64(key_data)
  352. data = self.decrypt_key_file(cdata, passphrase)
  353. if data:
  354. data = msgpack.unpackb(data)
  355. key = Key(internal_dict=data)
  356. if key.version not in (1, 2): # legacy: item.Key can still process v1 keys
  357. raise UnsupportedKeyFormatError()
  358. self.repository_id = key.repository_id
  359. self.crypt_key = key.crypt_key
  360. self.id_key = key.id_key
  361. self.chunk_seed = key.chunk_seed
  362. self.tam_required = key.get("tam_required", tam_required(self.repository))
  363. return True
  364. return False
  365. def decrypt_key_file(self, data, passphrase):
  366. unpacker = get_limited_unpacker("key")
  367. unpacker.feed(data)
  368. data = unpacker.unpack()
  369. encrypted_key = EncryptedKey(internal_dict=data)
  370. if encrypted_key.version != 1:
  371. raise UnsupportedKeyFormatError()
  372. else:
  373. self._encrypted_key_algorithm = encrypted_key.algorithm
  374. if encrypted_key.algorithm == "sha256":
  375. return self.decrypt_key_file_pbkdf2(encrypted_key, passphrase)
  376. elif encrypted_key.algorithm == "argon2 chacha20-poly1305":
  377. return self.decrypt_key_file_argon2(encrypted_key, passphrase)
  378. else:
  379. raise UnsupportedKeyFormatError()
  380. @staticmethod
  381. def pbkdf2(passphrase, salt, iterations, output_len_in_bytes):
  382. if os.environ.get("BORG_TESTONLY_WEAKEN_KDF") == "1":
  383. iterations = 1
  384. return pbkdf2_hmac("sha256", passphrase.encode("utf-8"), salt, iterations, output_len_in_bytes)
  385. @staticmethod
  386. def argon2(
  387. passphrase: str,
  388. output_len_in_bytes: int,
  389. salt: bytes,
  390. time_cost: int,
  391. memory_cost: int,
  392. parallelism: int,
  393. type: Literal["i", "d", "id"],
  394. ) -> bytes:
  395. if os.environ.get("BORG_TESTONLY_WEAKEN_KDF") == "1":
  396. time_cost = 1
  397. parallelism = 1
  398. # 8 is the smallest value that avoids the "Memory cost is too small" exception
  399. memory_cost = 8
  400. type_map = {"i": argon2.low_level.Type.I, "d": argon2.low_level.Type.D, "id": argon2.low_level.Type.ID}
  401. key = argon2.low_level.hash_secret_raw(
  402. secret=passphrase.encode("utf-8"),
  403. hash_len=output_len_in_bytes,
  404. salt=salt,
  405. time_cost=time_cost,
  406. memory_cost=memory_cost,
  407. parallelism=parallelism,
  408. type=type_map[type],
  409. )
  410. return key
  411. def decrypt_key_file_pbkdf2(self, encrypted_key, passphrase):
  412. key = self.pbkdf2(passphrase, encrypted_key.salt, encrypted_key.iterations, 32)
  413. data = AES(key, b"\0" * 16).decrypt(encrypted_key.data)
  414. if hmac.compare_digest(hmac_sha256(key, data), encrypted_key.hash):
  415. return data
  416. return None
  417. def decrypt_key_file_argon2(self, encrypted_key, passphrase):
  418. key = self.argon2(
  419. passphrase,
  420. output_len_in_bytes=32,
  421. salt=encrypted_key.salt,
  422. time_cost=encrypted_key.argon2_time_cost,
  423. memory_cost=encrypted_key.argon2_memory_cost,
  424. parallelism=encrypted_key.argon2_parallelism,
  425. type=encrypted_key.argon2_type,
  426. )
  427. ae_cipher = CHACHA20_POLY1305(key=key, iv=0, header_len=0, aad_offset=0)
  428. try:
  429. return ae_cipher.decrypt(encrypted_key.data)
  430. except low_level.IntegrityError:
  431. return None
  432. def encrypt_key_file(self, data, passphrase, algorithm):
  433. if algorithm == "sha256":
  434. return self.encrypt_key_file_pbkdf2(data, passphrase)
  435. elif algorithm == "argon2 chacha20-poly1305":
  436. return self.encrypt_key_file_argon2(data, passphrase)
  437. else:
  438. raise ValueError(f"Unexpected algorithm: {algorithm}")
  439. def encrypt_key_file_pbkdf2(self, data, passphrase):
  440. salt = os.urandom(32)
  441. iterations = PBKDF2_ITERATIONS
  442. key = self.pbkdf2(passphrase, salt, iterations, 32)
  443. hash = hmac_sha256(key, data)
  444. cdata = AES(key, b"\0" * 16).encrypt(data)
  445. enc_key = EncryptedKey(version=1, salt=salt, iterations=iterations, algorithm="sha256", hash=hash, data=cdata)
  446. return msgpack.packb(enc_key.as_dict())
  447. def encrypt_key_file_argon2(self, data, passphrase):
  448. salt = os.urandom(ARGON2_SALT_BYTES)
  449. key = self.argon2(passphrase, output_len_in_bytes=32, salt=salt, **ARGON2_ARGS)
  450. ae_cipher = CHACHA20_POLY1305(key=key, iv=0, header_len=0, aad_offset=0)
  451. encrypted_key = EncryptedKey(
  452. version=1,
  453. algorithm="argon2 chacha20-poly1305",
  454. salt=salt,
  455. data=ae_cipher.encrypt(data),
  456. **{"argon2_" + k: v for k, v in ARGON2_ARGS.items()},
  457. )
  458. return msgpack.packb(encrypted_key.as_dict())
  459. def _save(self, passphrase, algorithm):
  460. key = Key(
  461. version=2,
  462. repository_id=self.repository_id,
  463. crypt_key=self.crypt_key,
  464. id_key=self.id_key,
  465. chunk_seed=self.chunk_seed,
  466. tam_required=self.tam_required,
  467. )
  468. data = self.encrypt_key_file(msgpack.packb(key.as_dict()), passphrase, algorithm)
  469. key_data = "\n".join(textwrap.wrap(b2a_base64(data).decode("ascii")))
  470. return key_data
  471. def change_passphrase(self, passphrase=None):
  472. if passphrase is None:
  473. passphrase = Passphrase.new(allow_empty=True)
  474. self.save(self.target, passphrase, algorithm=self._encrypted_key_algorithm)
  475. @classmethod
  476. def create(cls, repository, args, *, other_key=None):
  477. key = cls(repository)
  478. key.repository_id = repository.id
  479. if other_key is not None:
  480. if isinstance(other_key, PlaintextKey):
  481. raise Error("Copying key material from an unencrypted repository is not possible.")
  482. if isinstance(key, AESKeyBase):
  483. # user must use an AEADKeyBase subclass (AEAD modes with session keys)
  484. raise Error("Copying key material to an AES-CTR based mode is insecure and unsupported.")
  485. if not uses_same_id_hash(other_key, key):
  486. raise Error("You must keep the same ID hash (HMAC-SHA256 or BLAKE2b) or deduplication will break.")
  487. if other_key.copy_crypt_key:
  488. # give the user the option to use the same authenticated encryption (AE) key
  489. crypt_key = other_key.crypt_key
  490. else:
  491. # borg transfer re-encrypts all data anyway, thus we can default to a new, random AE key
  492. crypt_key = os.urandom(64)
  493. key.init_from_given_data(crypt_key=crypt_key, id_key=other_key.id_key, chunk_seed=other_key.chunk_seed)
  494. passphrase = other_key._passphrase
  495. else:
  496. key.init_from_random_data()
  497. passphrase = Passphrase.new(allow_empty=True)
  498. key.init_ciphers()
  499. target = key.get_new_target(args)
  500. key.save(target, passphrase, create=True, algorithm=KEY_ALGORITHMS["argon2"])
  501. logger.info('Key in "%s" created.' % target)
  502. logger.info("Keep this key safe. Your data will be inaccessible without it.")
  503. return key
  504. def sanity_check(self, filename, id):
  505. file_id = self.FILE_ID.encode() + b" "
  506. repo_id = hexlify(id)
  507. with open(filename, "rb") as fd:
  508. # we do the magic / id check in binary mode to avoid stumbling over
  509. # decoding errors if somebody has binary files in the keys dir for some reason.
  510. if fd.read(len(file_id)) != file_id:
  511. raise KeyfileInvalidError(self.repository._location.canonical_path(), filename)
  512. if fd.read(len(repo_id)) != repo_id:
  513. raise KeyfileMismatchError(self.repository._location.canonical_path(), filename)
  514. # we get here if it really looks like a borg key for this repo,
  515. # do some more checks that are close to how borg reads/parses the key.
  516. with open(filename, "r") as fd:
  517. lines = fd.readlines()
  518. if len(lines) < 2:
  519. logger.warning(f"borg key sanity check: expected 2+ lines total. [{filename}]")
  520. raise KeyfileInvalidError(self.repository._location.canonical_path(), filename)
  521. if len(lines[0].rstrip()) > len(file_id) + len(repo_id):
  522. logger.warning(f"borg key sanity check: key line 1 seems too long. [{filename}]")
  523. raise KeyfileInvalidError(self.repository._location.canonical_path(), filename)
  524. key_b64 = "".join(lines[1:])
  525. try:
  526. key = a2b_base64(key_b64)
  527. except binascii.Error:
  528. logger.warning(f"borg key sanity check: key line 2+ does not look like base64. [{filename}]")
  529. raise KeyfileInvalidError(self.repository._location.canonical_path(), filename)
  530. if len(key) < 20:
  531. # this is in no way a precise check, usually we have about 400b key data.
  532. logger.warning(
  533. f"borg key sanity check: binary encrypted key data from key line 2+ suspiciously short."
  534. f" [{filename}]"
  535. )
  536. raise KeyfileInvalidError(self.repository._location.canonical_path(), filename)
  537. # looks good!
  538. return filename
  539. def find_key(self):
  540. if self.STORAGE == KeyBlobStorage.KEYFILE:
  541. keyfile = self._find_key_file_from_environment()
  542. if keyfile is not None:
  543. return self.sanity_check(keyfile, self.repository.id)
  544. keyfile = self._find_key_in_keys_dir()
  545. if keyfile is not None:
  546. return keyfile
  547. raise KeyfileNotFoundError(self.repository._location.canonical_path(), get_keys_dir())
  548. elif self.STORAGE == KeyBlobStorage.REPO:
  549. loc = self.repository._location.canonical_path()
  550. key = self.repository.load_key()
  551. if not key:
  552. # if we got an empty key, it means there is no key.
  553. raise RepoKeyNotFoundError(loc) from None
  554. return loc
  555. else:
  556. raise TypeError("Unsupported borg key storage type")
  557. def get_existing_or_new_target(self, args):
  558. keyfile = self._find_key_file_from_environment()
  559. if keyfile is not None:
  560. return keyfile
  561. keyfile = self._find_key_in_keys_dir()
  562. if keyfile is not None:
  563. return keyfile
  564. return self._get_new_target_in_keys_dir(args)
  565. def _find_key_in_keys_dir(self):
  566. id = self.repository.id
  567. keys_dir = get_keys_dir()
  568. for name in os.listdir(keys_dir):
  569. filename = os.path.join(keys_dir, name)
  570. try:
  571. return self.sanity_check(filename, id)
  572. except (KeyfileInvalidError, KeyfileMismatchError):
  573. pass
  574. def get_new_target(self, args):
  575. if self.STORAGE == KeyBlobStorage.KEYFILE:
  576. keyfile = self._find_key_file_from_environment()
  577. if keyfile is not None:
  578. return keyfile
  579. return self._get_new_target_in_keys_dir(args)
  580. elif self.STORAGE == KeyBlobStorage.REPO:
  581. return self.repository
  582. else:
  583. raise TypeError("Unsupported borg key storage type")
  584. def _find_key_file_from_environment(self):
  585. keyfile = os.environ.get("BORG_KEY_FILE")
  586. if keyfile:
  587. return os.path.abspath(keyfile)
  588. def _get_new_target_in_keys_dir(self, args):
  589. filename = args.location.to_key_filename()
  590. path = filename
  591. i = 1
  592. while os.path.exists(path):
  593. i += 1
  594. path = filename + ".%d" % i
  595. return path
  596. def load(self, target, passphrase):
  597. if self.STORAGE == KeyBlobStorage.KEYFILE:
  598. with open(target) as fd:
  599. key_data = "".join(fd.readlines()[1:])
  600. elif self.STORAGE == KeyBlobStorage.REPO:
  601. # While the repository is encrypted, we consider a repokey repository with a blank
  602. # passphrase an unencrypted repository.
  603. self.logically_encrypted = passphrase != ""
  604. # what we get in target is just a repo location, but we already have the repo obj:
  605. target = self.repository
  606. key_data = target.load_key()
  607. if not key_data:
  608. # if we got an empty key, it means there is no key.
  609. loc = target._location.canonical_path()
  610. raise RepoKeyNotFoundError(loc) from None
  611. key_data = key_data.decode("utf-8") # remote repo: msgpack issue #99, getting bytes
  612. else:
  613. raise TypeError("Unsupported borg key storage type")
  614. success = self._load(key_data, passphrase)
  615. if success:
  616. self.target = target
  617. return success
  618. def save(self, target, passphrase, algorithm, create=False):
  619. key_data = self._save(passphrase, algorithm)
  620. if self.STORAGE == KeyBlobStorage.KEYFILE:
  621. if create and os.path.isfile(target):
  622. # if a new keyfile key repository is created, ensure that an existing keyfile of another
  623. # keyfile key repo is not accidentally overwritten by careless use of the BORG_KEY_FILE env var.
  624. # see issue #6036
  625. raise Error('Aborting because key in "%s" already exists.' % target)
  626. with SaveFile(target) as fd:
  627. fd.write(f"{self.FILE_ID} {bin_to_hex(self.repository_id)}\n")
  628. fd.write(key_data)
  629. fd.write("\n")
  630. elif self.STORAGE == KeyBlobStorage.REPO:
  631. self.logically_encrypted = passphrase != ""
  632. key_data = key_data.encode("utf-8") # remote repo: msgpack issue #99, giving bytes
  633. target.save_key(key_data)
  634. else:
  635. raise TypeError("Unsupported borg key storage type")
  636. self.target = target
  637. def remove(self, target):
  638. if self.STORAGE == KeyBlobStorage.KEYFILE:
  639. os.remove(target)
  640. elif self.STORAGE == KeyBlobStorage.REPO:
  641. target.save_key(b"") # save empty key (no new api at remote repo necessary)
  642. else:
  643. raise TypeError("Unsupported borg key storage type")
  644. class KeyfileKey(ID_HMAC_SHA_256, AESKeyBase, FlexiKey):
  645. TYPES_ACCEPTABLE = {KeyType.KEYFILE, KeyType.REPO, KeyType.PASSPHRASE}
  646. TYPE = KeyType.KEYFILE
  647. NAME = "key file"
  648. ARG_NAME = "keyfile"
  649. STORAGE = KeyBlobStorage.KEYFILE
  650. CIPHERSUITE = AES256_CTR_HMAC_SHA256
  651. class RepoKey(ID_HMAC_SHA_256, AESKeyBase, FlexiKey):
  652. TYPES_ACCEPTABLE = {KeyType.KEYFILE, KeyType.REPO, KeyType.PASSPHRASE}
  653. TYPE = KeyType.REPO
  654. NAME = "repokey"
  655. ARG_NAME = "repokey"
  656. STORAGE = KeyBlobStorage.REPO
  657. CIPHERSUITE = AES256_CTR_HMAC_SHA256
  658. class Blake2KeyfileKey(ID_BLAKE2b_256, AESKeyBase, FlexiKey):
  659. TYPES_ACCEPTABLE = {KeyType.BLAKE2KEYFILE, KeyType.BLAKE2REPO}
  660. TYPE = KeyType.BLAKE2KEYFILE
  661. NAME = "key file BLAKE2b"
  662. ARG_NAME = "keyfile-blake2"
  663. STORAGE = KeyBlobStorage.KEYFILE
  664. CIPHERSUITE = AES256_CTR_BLAKE2b
  665. class Blake2RepoKey(ID_BLAKE2b_256, AESKeyBase, FlexiKey):
  666. TYPES_ACCEPTABLE = {KeyType.BLAKE2KEYFILE, KeyType.BLAKE2REPO}
  667. TYPE = KeyType.BLAKE2REPO
  668. NAME = "repokey BLAKE2b"
  669. ARG_NAME = "repokey-blake2"
  670. STORAGE = KeyBlobStorage.REPO
  671. CIPHERSUITE = AES256_CTR_BLAKE2b
  672. class AuthenticatedKeyBase(AESKeyBase, FlexiKey):
  673. STORAGE = KeyBlobStorage.REPO
  674. # It's only authenticated, not encrypted.
  675. logically_encrypted = False
  676. def _load(self, key_data, passphrase):
  677. if AUTHENTICATED_NO_KEY:
  678. # fake _load if we have no key or passphrase
  679. NOPE = bytes(32) # 256 bit all-zero
  680. self.repository_id = NOPE
  681. self.enc_key = NOPE
  682. self.enc_hmac_key = NOPE
  683. self.id_key = NOPE
  684. self.chunk_seed = 0
  685. self.tam_required = False
  686. return True
  687. return super()._load(key_data, passphrase)
  688. def load(self, target, passphrase):
  689. success = super().load(target, passphrase)
  690. self.logically_encrypted = False
  691. return success
  692. def save(self, target, passphrase, algorithm, create=False):
  693. super().save(target, passphrase, algorithm, create=create)
  694. self.logically_encrypted = False
  695. def init_ciphers(self, manifest_data=None):
  696. if manifest_data is not None:
  697. self.assert_type(manifest_data[0])
  698. def encrypt(self, id, data):
  699. return b"".join([self.TYPE_STR, data])
  700. def decrypt(self, id, data):
  701. self.assert_type(data[0], id)
  702. return memoryview(data)[1:]
  703. class AuthenticatedKey(ID_HMAC_SHA_256, AuthenticatedKeyBase):
  704. TYPE = KeyType.AUTHENTICATED
  705. TYPES_ACCEPTABLE = {TYPE}
  706. NAME = "authenticated"
  707. ARG_NAME = "authenticated"
  708. class Blake2AuthenticatedKey(ID_BLAKE2b_256, AuthenticatedKeyBase):
  709. TYPE = KeyType.BLAKE2AUTHENTICATED
  710. TYPES_ACCEPTABLE = {TYPE}
  711. NAME = "authenticated BLAKE2b"
  712. ARG_NAME = "authenticated-blake2"
  713. # ------------ new crypto ------------
  714. class AEADKeyBase(KeyBase):
  715. """
  716. Chunks are encrypted and authenticated using some AEAD ciphersuite
  717. Layout: suite:4 keytype:4 reserved:8 messageIV:48 sessionID:192 auth_tag:128 payload:... [bits]
  718. ^-------------------- AAD ----------------------------^
  719. Offsets:0 1 2 8 32 48 [bytes]
  720. suite: 1010b for new AEAD crypto, 0000b is old crypto
  721. keytype: see constants.KeyType (suite+keytype)
  722. reserved: all-zero, for future use
  723. messageIV: a counter starting from 0 for all new encrypted messages of one session
  724. sessionID: 192bit random, computed once per session (the session key is derived from this)
  725. auth_tag: authentication tag output of the AEAD cipher (computed over payload and AAD)
  726. payload: encrypted chunk data
  727. """
  728. PAYLOAD_OVERHEAD = 1 + 1 + 6 + 24 + 16 # [bytes], see Layout
  729. CIPHERSUITE: Callable = None # override in subclass
  730. logically_encrypted = True
  731. MAX_IV = 2**48 - 1
  732. def assert_id(self, id, data):
  733. # Comparing the id hash here would not be needed any more for the new AEAD crypto **IF** we
  734. # could be sure that chunks were created by normal (not tampered, not evil) borg code:
  735. # We put the id into AAD when storing the chunk, so it gets into the authentication tag computation.
  736. # when decrypting, we provide the id we **want** as AAD for the auth tag verification, so
  737. # decrypting only succeeds if we got the ciphertext we wrote **for that chunk id**.
  738. # So, basically the **repository** can not cheat on us by giving us a different chunk.
  739. #
  740. # **BUT**, if chunks are created by tampered, evil borg code, the borg client code could put
  741. # a wrong chunkid into AAD and then AEAD-encrypt-and-auth this and store it into the
  742. # repository using this bad chunkid as key (violating the usual chunkid == id_hash(data)).
  743. # Later, when reading such a bad chunk, AEAD-auth-and-decrypt would not notice any
  744. # issue and decrypt successfully.
  745. # Thus, to notice such evil borg activity, we must check for such violations here:
  746. if id and id != Manifest.MANIFEST_ID:
  747. id_computed = self.id_hash(data)
  748. if not hmac.compare_digest(id_computed, id):
  749. raise IntegrityError("Chunk %s: id verification failed" % bin_to_hex(id))
  750. def encrypt(self, id, data):
  751. # to encrypt new data in this session we use always self.cipher and self.sessionid
  752. reserved = b"\0"
  753. iv = self.cipher.next_iv()
  754. if iv > self.MAX_IV: # see the data-structures docs about why the IV range is enough
  755. raise IntegrityError("IV overflow, should never happen.")
  756. iv_48bit = iv.to_bytes(6, "big")
  757. header = self.TYPE_STR + reserved + iv_48bit + self.sessionid
  758. return self.cipher.encrypt(data, header=header, iv=iv, aad=id)
  759. def decrypt(self, id, data):
  760. # to decrypt existing data, we need to get a cipher configured for the sessionid and iv from header
  761. self.assert_type(data[0], id)
  762. iv_48bit = data[2:8]
  763. sessionid = data[8:32]
  764. iv = int.from_bytes(iv_48bit, "big")
  765. cipher = self._get_cipher(sessionid, iv)
  766. try:
  767. return cipher.decrypt(data, aad=id)
  768. except IntegrityError as e:
  769. raise IntegrityError(f"Chunk {bin_to_hex(id)}: Could not decrypt [{str(e)}]")
  770. def init_from_given_data(self, *, crypt_key, id_key, chunk_seed):
  771. assert len(crypt_key) in (32 + 32, 32 + 128)
  772. assert len(id_key) in (32, 128)
  773. assert isinstance(chunk_seed, int)
  774. self.crypt_key = crypt_key
  775. self.id_key = id_key
  776. self.chunk_seed = chunk_seed
  777. def init_from_random_data(self):
  778. data = os.urandom(100)
  779. chunk_seed = bytes_to_int(data[96:100])
  780. # Convert to signed int32
  781. if chunk_seed & 0x80000000:
  782. chunk_seed = chunk_seed - 0xFFFFFFFF - 1
  783. self.init_from_given_data(crypt_key=data[0:64], id_key=data[64:96], chunk_seed=chunk_seed)
  784. def _get_session_key(self, sessionid):
  785. assert len(sessionid) == 24 # 192bit
  786. key = hkdf_hmac_sha512(
  787. ikm=self.crypt_key,
  788. salt=sessionid,
  789. info=b"borg-session-key-" + self.CIPHERSUITE.__name__.encode(),
  790. output_length=32,
  791. )
  792. return key
  793. def _get_cipher(self, sessionid, iv):
  794. assert isinstance(iv, int)
  795. key = self._get_session_key(sessionid)
  796. cipher = self.CIPHERSUITE(key=key, iv=iv, header_len=1 + 1 + 6 + 24, aad_offset=0)
  797. return cipher
  798. def init_ciphers(self, manifest_data=None, iv=0):
  799. # in every new session we start with a fresh sessionid and at iv == 0, manifest_data and iv params are ignored
  800. self.sessionid = os.urandom(24)
  801. self.cipher = self._get_cipher(self.sessionid, iv=0)
  802. class AESOCBKeyfileKey(ID_HMAC_SHA_256, AEADKeyBase, FlexiKey):
  803. TYPES_ACCEPTABLE = {KeyType.AESOCBKEYFILE, KeyType.AESOCBREPO}
  804. TYPE = KeyType.AESOCBKEYFILE
  805. NAME = "key file AES-OCB"
  806. ARG_NAME = "keyfile-aes-ocb"
  807. STORAGE = KeyBlobStorage.KEYFILE
  808. CIPHERSUITE = AES256_OCB
  809. class AESOCBRepoKey(ID_HMAC_SHA_256, AEADKeyBase, FlexiKey):
  810. TYPES_ACCEPTABLE = {KeyType.AESOCBKEYFILE, KeyType.AESOCBREPO}
  811. TYPE = KeyType.AESOCBREPO
  812. NAME = "repokey AES-OCB"
  813. ARG_NAME = "repokey-aes-ocb"
  814. STORAGE = KeyBlobStorage.REPO
  815. CIPHERSUITE = AES256_OCB
  816. class CHPOKeyfileKey(ID_HMAC_SHA_256, AEADKeyBase, FlexiKey):
  817. TYPES_ACCEPTABLE = {KeyType.CHPOKEYFILE, KeyType.CHPOREPO}
  818. TYPE = KeyType.CHPOKEYFILE
  819. NAME = "key file ChaCha20-Poly1305"
  820. ARG_NAME = "keyfile-chacha20-poly1305"
  821. STORAGE = KeyBlobStorage.KEYFILE
  822. CIPHERSUITE = CHACHA20_POLY1305
  823. class CHPORepoKey(ID_HMAC_SHA_256, AEADKeyBase, FlexiKey):
  824. TYPES_ACCEPTABLE = {KeyType.CHPOKEYFILE, KeyType.CHPOREPO}
  825. TYPE = KeyType.CHPOREPO
  826. NAME = "repokey ChaCha20-Poly1305"
  827. ARG_NAME = "repokey-chacha20-poly1305"
  828. STORAGE = KeyBlobStorage.REPO
  829. CIPHERSUITE = CHACHA20_POLY1305
  830. class Blake2AESOCBKeyfileKey(ID_BLAKE2b_256, AEADKeyBase, FlexiKey):
  831. TYPES_ACCEPTABLE = {KeyType.BLAKE2AESOCBKEYFILE, KeyType.BLAKE2AESOCBREPO}
  832. TYPE = KeyType.BLAKE2AESOCBKEYFILE
  833. NAME = "key file BLAKE2b AES-OCB"
  834. ARG_NAME = "keyfile-blake2-aes-ocb"
  835. STORAGE = KeyBlobStorage.KEYFILE
  836. CIPHERSUITE = AES256_OCB
  837. class Blake2AESOCBRepoKey(ID_BLAKE2b_256, AEADKeyBase, FlexiKey):
  838. TYPES_ACCEPTABLE = {KeyType.BLAKE2AESOCBKEYFILE, KeyType.BLAKE2AESOCBREPO}
  839. TYPE = KeyType.BLAKE2AESOCBREPO
  840. NAME = "repokey BLAKE2b AES-OCB"
  841. ARG_NAME = "repokey-blake2-aes-ocb"
  842. STORAGE = KeyBlobStorage.REPO
  843. CIPHERSUITE = AES256_OCB
  844. class Blake2CHPOKeyfileKey(ID_BLAKE2b_256, AEADKeyBase, FlexiKey):
  845. TYPES_ACCEPTABLE = {KeyType.BLAKE2CHPOKEYFILE, KeyType.BLAKE2CHPOREPO}
  846. TYPE = KeyType.BLAKE2CHPOKEYFILE
  847. NAME = "key file BLAKE2b ChaCha20-Poly1305"
  848. ARG_NAME = "keyfile-blake2-chacha20-poly1305"
  849. STORAGE = KeyBlobStorage.KEYFILE
  850. CIPHERSUITE = CHACHA20_POLY1305
  851. class Blake2CHPORepoKey(ID_BLAKE2b_256, AEADKeyBase, FlexiKey):
  852. TYPES_ACCEPTABLE = {KeyType.BLAKE2CHPOKEYFILE, KeyType.BLAKE2CHPOREPO}
  853. TYPE = KeyType.BLAKE2CHPOREPO
  854. NAME = "repokey BLAKE2b ChaCha20-Poly1305"
  855. ARG_NAME = "repokey-blake2-chacha20-poly1305"
  856. STORAGE = KeyBlobStorage.REPO
  857. CIPHERSUITE = CHACHA20_POLY1305
  858. LEGACY_KEY_TYPES = (
  859. # legacy (AES-CTR based) crypto
  860. KeyfileKey,
  861. RepoKey,
  862. Blake2KeyfileKey,
  863. Blake2RepoKey,
  864. )
  865. AVAILABLE_KEY_TYPES = (
  866. # these are available encryption modes for new repositories
  867. # not encrypted modes
  868. PlaintextKey,
  869. AuthenticatedKey,
  870. Blake2AuthenticatedKey,
  871. # new crypto
  872. AESOCBKeyfileKey,
  873. AESOCBRepoKey,
  874. CHPOKeyfileKey,
  875. CHPORepoKey,
  876. Blake2AESOCBKeyfileKey,
  877. Blake2AESOCBRepoKey,
  878. Blake2CHPOKeyfileKey,
  879. Blake2CHPORepoKey,
  880. )