Funktionierender Prototyp des Serious Games zur Vermittlung von Wissen zu Software-Engineering-Arbeitsmodellen.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ckeygen.py 11KB

1 year ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. # -*- test-case-name: twisted.conch.test.test_ckeygen -*-
  2. # Copyright (c) Twisted Matrix Laboratories.
  3. # See LICENSE for details.
  4. """
  5. Implementation module for the `ckeygen` command.
  6. """
  7. import getpass
  8. import os
  9. import socket
  10. import sys
  11. from functools import wraps
  12. from imp import reload
  13. from twisted.conch.ssh import keys
  14. from twisted.python import failure, filepath, log, usage
  15. if getpass.getpass == getpass.unix_getpass: # type: ignore[attr-defined]
  16. try:
  17. import termios # hack around broken termios
  18. termios.tcgetattr, termios.tcsetattr
  19. except (ImportError, AttributeError):
  20. sys.modules["termios"] = None # type: ignore[assignment]
  21. reload(getpass)
  22. supportedKeyTypes = dict()
  23. def _keyGenerator(keyType):
  24. def assignkeygenerator(keygenerator):
  25. @wraps(keygenerator)
  26. def wrapper(*args, **kwargs):
  27. return keygenerator(*args, **kwargs)
  28. supportedKeyTypes[keyType] = wrapper
  29. return wrapper
  30. return assignkeygenerator
  31. class GeneralOptions(usage.Options):
  32. synopsis = """Usage: ckeygen [options]
  33. """
  34. longdesc = "ckeygen manipulates public/private keys in various ways."
  35. optParameters = [
  36. ["bits", "b", None, "Number of bits in the key to create."],
  37. ["filename", "f", None, "Filename of the key file."],
  38. ["type", "t", None, "Specify type of key to create."],
  39. ["comment", "C", None, "Provide new comment."],
  40. ["newpass", "N", None, "Provide new passphrase."],
  41. ["pass", "P", None, "Provide old passphrase."],
  42. ["format", "o", "sha256-base64", "Fingerprint format of key file."],
  43. [
  44. "private-key-subtype",
  45. None,
  46. None,
  47. 'OpenSSH private key subtype to write ("PEM" or "v1").',
  48. ],
  49. ]
  50. optFlags = [
  51. ["fingerprint", "l", "Show fingerprint of key file."],
  52. ["changepass", "p", "Change passphrase of private key file."],
  53. ["quiet", "q", "Quiet."],
  54. ["no-passphrase", None, "Create the key with no passphrase."],
  55. ["showpub", "y", "Read private key file and print public key."],
  56. ]
  57. compData = usage.Completions(
  58. optActions={
  59. "type": usage.CompleteList(list(supportedKeyTypes.keys())),
  60. "private-key-subtype": usage.CompleteList(["PEM", "v1"]),
  61. }
  62. )
  63. def run():
  64. options = GeneralOptions()
  65. try:
  66. options.parseOptions(sys.argv[1:])
  67. except usage.UsageError as u:
  68. print("ERROR: %s" % u)
  69. options.opt_help()
  70. sys.exit(1)
  71. log.discardLogs()
  72. log.deferr = handleError # HACK
  73. if options["type"]:
  74. if options["type"].lower() in supportedKeyTypes:
  75. print("Generating public/private %s key pair." % (options["type"]))
  76. supportedKeyTypes[options["type"].lower()](options)
  77. else:
  78. sys.exit(
  79. "Key type was %s, must be one of %s"
  80. % (options["type"], ", ".join(supportedKeyTypes.keys()))
  81. )
  82. elif options["fingerprint"]:
  83. printFingerprint(options)
  84. elif options["changepass"]:
  85. changePassPhrase(options)
  86. elif options["showpub"]:
  87. displayPublicKey(options)
  88. else:
  89. options.opt_help()
  90. sys.exit(1)
  91. def enumrepresentation(options):
  92. if options["format"] == "md5-hex":
  93. options["format"] = keys.FingerprintFormats.MD5_HEX
  94. return options
  95. elif options["format"] == "sha256-base64":
  96. options["format"] = keys.FingerprintFormats.SHA256_BASE64
  97. return options
  98. else:
  99. raise keys.BadFingerPrintFormat(
  100. "Unsupported fingerprint format: {}".format(options["format"])
  101. )
  102. def handleError():
  103. global exitStatus
  104. exitStatus = 2
  105. log.err(failure.Failure())
  106. raise
  107. @_keyGenerator("rsa")
  108. def generateRSAkey(options):
  109. from cryptography.hazmat.backends import default_backend
  110. from cryptography.hazmat.primitives.asymmetric import rsa
  111. if not options["bits"]:
  112. options["bits"] = 2048
  113. keyPrimitive = rsa.generate_private_key(
  114. key_size=int(options["bits"]),
  115. public_exponent=65537,
  116. backend=default_backend(),
  117. )
  118. key = keys.Key(keyPrimitive)
  119. _saveKey(key, options)
  120. @_keyGenerator("dsa")
  121. def generateDSAkey(options):
  122. from cryptography.hazmat.backends import default_backend
  123. from cryptography.hazmat.primitives.asymmetric import dsa
  124. if not options["bits"]:
  125. options["bits"] = 1024
  126. keyPrimitive = dsa.generate_private_key(
  127. key_size=int(options["bits"]),
  128. backend=default_backend(),
  129. )
  130. key = keys.Key(keyPrimitive)
  131. _saveKey(key, options)
  132. @_keyGenerator("ecdsa")
  133. def generateECDSAkey(options):
  134. from cryptography.hazmat.backends import default_backend
  135. from cryptography.hazmat.primitives.asymmetric import ec
  136. if not options["bits"]:
  137. options["bits"] = 256
  138. # OpenSSH supports only mandatory sections of RFC5656.
  139. # See https://www.openssh.com/txt/release-5.7
  140. curve = b"ecdsa-sha2-nistp" + str(options["bits"]).encode("ascii")
  141. keyPrimitive = ec.generate_private_key(
  142. curve=keys._curveTable[curve], backend=default_backend()
  143. )
  144. key = keys.Key(keyPrimitive)
  145. _saveKey(key, options)
  146. @_keyGenerator("ed25519")
  147. def generateEd25519key(options):
  148. keyPrimitive = keys.Ed25519PrivateKey.generate()
  149. key = keys.Key(keyPrimitive)
  150. _saveKey(key, options)
  151. def _defaultPrivateKeySubtype(keyType):
  152. """
  153. Return a reasonable default private key subtype for a given key type.
  154. @type keyType: L{str}
  155. @param keyType: A key type, as returned by
  156. L{twisted.conch.ssh.keys.Key.type}.
  157. @rtype: L{str}
  158. @return: A private OpenSSH key subtype (C{'PEM'} or C{'v1'}).
  159. """
  160. if keyType == "Ed25519":
  161. # No PEM format is defined for Ed25519 keys.
  162. return "v1"
  163. else:
  164. return "PEM"
  165. def printFingerprint(options):
  166. if not options["filename"]:
  167. filename = os.path.expanduser("~/.ssh/id_rsa")
  168. options["filename"] = input("Enter file in which the key is (%s): " % filename)
  169. if os.path.exists(options["filename"] + ".pub"):
  170. options["filename"] += ".pub"
  171. options = enumrepresentation(options)
  172. try:
  173. key = keys.Key.fromFile(options["filename"])
  174. print(
  175. "%s %s %s"
  176. % (
  177. key.size(),
  178. key.fingerprint(options["format"]),
  179. os.path.basename(options["filename"]),
  180. )
  181. )
  182. except keys.BadKeyError:
  183. sys.exit("bad key")
  184. def changePassPhrase(options):
  185. if not options["filename"]:
  186. filename = os.path.expanduser("~/.ssh/id_rsa")
  187. options["filename"] = input("Enter file in which the key is (%s): " % filename)
  188. try:
  189. key = keys.Key.fromFile(options["filename"])
  190. except keys.EncryptedKeyError:
  191. # Raised if password not supplied for an encrypted key
  192. if not options.get("pass"):
  193. options["pass"] = getpass.getpass("Enter old passphrase: ")
  194. try:
  195. key = keys.Key.fromFile(options["filename"], passphrase=options["pass"])
  196. except keys.BadKeyError:
  197. sys.exit("Could not change passphrase: old passphrase error")
  198. except keys.EncryptedKeyError as e:
  199. sys.exit(f"Could not change passphrase: {e}")
  200. except keys.BadKeyError as e:
  201. sys.exit(f"Could not change passphrase: {e}")
  202. if not options.get("newpass"):
  203. while 1:
  204. p1 = getpass.getpass("Enter new passphrase (empty for no passphrase): ")
  205. p2 = getpass.getpass("Enter same passphrase again: ")
  206. if p1 == p2:
  207. break
  208. print("Passphrases do not match. Try again.")
  209. options["newpass"] = p1
  210. if options.get("private-key-subtype") is None:
  211. options["private-key-subtype"] = _defaultPrivateKeySubtype(key.type())
  212. try:
  213. newkeydata = key.toString(
  214. "openssh",
  215. subtype=options["private-key-subtype"],
  216. passphrase=options["newpass"],
  217. )
  218. except Exception as e:
  219. sys.exit(f"Could not change passphrase: {e}")
  220. try:
  221. keys.Key.fromString(newkeydata, passphrase=options["newpass"])
  222. except (keys.EncryptedKeyError, keys.BadKeyError) as e:
  223. sys.exit(f"Could not change passphrase: {e}")
  224. with open(options["filename"], "wb") as fd:
  225. fd.write(newkeydata)
  226. print("Your identification has been saved with the new passphrase.")
  227. def displayPublicKey(options):
  228. if not options["filename"]:
  229. filename = os.path.expanduser("~/.ssh/id_rsa")
  230. options["filename"] = input("Enter file in which the key is (%s): " % filename)
  231. try:
  232. key = keys.Key.fromFile(options["filename"])
  233. except keys.EncryptedKeyError:
  234. if not options.get("pass"):
  235. options["pass"] = getpass.getpass("Enter passphrase: ")
  236. key = keys.Key.fromFile(options["filename"], passphrase=options["pass"])
  237. displayKey = key.public().toString("openssh").decode("ascii")
  238. print(displayKey)
  239. def _inputSaveFile(prompt: str) -> str:
  240. """
  241. Ask the user where to save the key.
  242. This needs to be a separate function so the unit test can patch it.
  243. """
  244. return input(prompt)
  245. def _saveKey(key, options):
  246. """
  247. Persist a SSH key on local filesystem.
  248. @param key: Key which is persisted on local filesystem.
  249. @type key: C{keys.Key} implementation.
  250. @param options:
  251. @type options: L{dict}
  252. """
  253. KeyTypeMapping = {"EC": "ecdsa", "Ed25519": "ed25519", "RSA": "rsa", "DSA": "dsa"}
  254. keyTypeName = KeyTypeMapping[key.type()]
  255. if not options["filename"]:
  256. defaultPath = os.path.expanduser(f"~/.ssh/id_{keyTypeName}")
  257. newPath = _inputSaveFile(
  258. f"Enter file in which to save the key ({defaultPath}): "
  259. )
  260. options["filename"] = newPath.strip() or defaultPath
  261. if os.path.exists(options["filename"]):
  262. print("{} already exists.".format(options["filename"]))
  263. yn = input("Overwrite (y/n)? ")
  264. if yn[0].lower() != "y":
  265. sys.exit()
  266. if options.get("no-passphrase"):
  267. options["pass"] = b""
  268. elif not options["pass"]:
  269. while 1:
  270. p1 = getpass.getpass("Enter passphrase (empty for no passphrase): ")
  271. p2 = getpass.getpass("Enter same passphrase again: ")
  272. if p1 == p2:
  273. break
  274. print("Passphrases do not match. Try again.")
  275. options["pass"] = p1
  276. if options.get("private-key-subtype") is None:
  277. options["private-key-subtype"] = _defaultPrivateKeySubtype(key.type())
  278. comment = f"{getpass.getuser()}@{socket.gethostname()}"
  279. filepath.FilePath(options["filename"]).setContent(
  280. key.toString(
  281. "openssh",
  282. subtype=options["private-key-subtype"],
  283. passphrase=options["pass"],
  284. )
  285. )
  286. os.chmod(options["filename"], 33152)
  287. filepath.FilePath(options["filename"] + ".pub").setContent(
  288. key.public().toString("openssh", comment=comment)
  289. )
  290. options = enumrepresentation(options)
  291. print("Your identification has been saved in {}".format(options["filename"]))
  292. print("Your public key has been saved in {}.pub".format(options["filename"]))
  293. print("The key fingerprint in {} is:".format(options["format"]))
  294. print(key.fingerprint(options["format"]))
  295. if __name__ == "__main__":
  296. run()