~jan0sch/smederee
Showing details for patch 72d8e10511a63dee4337be05bdff26523ee8e5ff.
diff -rN -u old-smederee/modules/hub/src/main/resources/db/migration/V1__base_tables.sql new-smederee/modules/hub/src/main/resources/db/migration/V1__base_tables.sql --- old-smederee/modules/hub/src/main/resources/db/migration/V1__base_tables.sql 2025-02-02 04:05:36.562634927 +0000 +++ new-smederee/modules/hub/src/main/resources/db/migration/V1__base_tables.sql 2025-02-02 04:05:36.562634927 +0000 @@ -60,6 +60,7 @@ ( "id" UUID NOT NULL, "uid" UUID NOT NULL, + "key_type" CHARACTER VARYING(32) NOT NULL, "key" TEXT NOT NULL, "fingerprint" CHARACTER VARYING(256) NOT NULL, "comment" CHARACTER VARYING(256) DEFAULT NULL, @@ -76,6 +77,7 @@ COMMENT ON TABLE "ssh_keys" IS 'SSH keys uploaded by users live within this table. Updates are not intended, so keys have to be deleted and re-uploaded upon changes.'; COMMENT ON COLUMN "ssh_keys"."id" IS 'The globally unique ID of the ssh key.'; COMMENT ON COLUMN "ssh_keys"."uid" IS 'The unique ID of the user account to whom the ssh key belongs.'; +COMMENT ON COLUMN "ssh_keys"."key_type" IS 'The type of the key e.g. ssh-rsa or ssh-ed25519 and so on.'; COMMENT ON COLUMN "ssh_keys"."key" IS 'A base 64 string containing the public ssh key.'; COMMENT ON COLUMN "ssh_keys"."fingerprint" IS 'The fingerprint of the ssh key. It must be unique because a key can only be used by one account.'; COMMENT ON COLUMN "ssh_keys"."comment" IS 'An optional comment for the ssh key limited to 256 characters.'; diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/ssh/PublicSshKey.scala new-smederee/modules/hub/src/main/scala/de/smederee/ssh/PublicSshKey.scala --- old-smederee/modules/hub/src/main/scala/de/smederee/ssh/PublicSshKey.scala 1970-01-01 00:00:00.000000000 +0000 +++ new-smederee/modules/hub/src/main/scala/de/smederee/ssh/PublicSshKey.scala 2025-02-02 04:05:36.566634932 +0000 @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2022 Contributors as noted in the AUTHORS.md file + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package de.smederee.ssh + +import java.security.PublicKey +import java.util.UUID + +import cats._ +import cats.syntax.all._ +import de.smederee.hub._ + +import scala.util.matching.Regex + +opaque type SshPublicKeyString = String +object SshPublicKeyString { + val Format: Regex = "^([\\w-]+)\\s(.+)(\\s(.+))?$".r + + /** Create an instance of SshPublicKeyString from the given String type. + * + * @param source + * An instance of type String which will be returned as a SshPublicKeyString. + * @return + * The appropriate instance of SshPublicKeyString. + */ + def apply(source: String): SshPublicKeyString = source + + /** Try to create an instance of SshPublicKeyString from the given String. + * + * @param source + * A String that should fulfil the requirements to be converted into a SshPublicKeyString. + * @return + * An option to the successfully converted SshPublicKeyString. + */ + def from(source: String): Option[SshPublicKeyString] = Option(source).filter(string => Format.matches(string)) +} + +/** Possible ssh key types. + */ +enum SshKeyType { + case SshDsa + + case SshEcDsa + + case SshEcDsaSk + + /** Recommended key type because smaller payload and more secure but some servers still don't support them. + */ + case SshEd25519 + + case SshEd25519Sk + + /** RSA keys are (still) the most common ones and the most compatible. + */ + case SshRsa +} + +object SshKeyType { + val Mappings: Map[String, SshKeyType] = Map( + "ssh-dss" -> SshDsa, + "ecdsa-sha2-nistp256" -> SshEcDsa, + "ssh-ed25519" -> SshEd25519, + "ssh-rsa" -> SshRsa + ) + + given Eq[SshKeyType] = Eq.fromUniversalEquals + + /** Try to extract a supported [[SshKeyType]] from the given string that should contain a valid ssh public key. + * According to RFC 4253 the key format is as follows: + * {{{ + * [type-name] [base-64-encoded-public-key] [comment] + * }}} + * While the comment is optional the other parts are required. + * + * @param sshKeyString + * A string containing a correctly formatted public ssh key. + * @return + * An option to the extracted key type. + */ + def from(sshKeyString: String): Option[SshKeyType] = { + val typeString = sshKeyString.takeWhile(char => !char.isWhitespace) + Mappings.get(typeString) + } +} + +/** Actual ssh key and related metadata describing the public ssh key of a user. + * + * @param id + * The globally unique id of the ssh key. + * @param ownerId + * The unique id of the user account associated with this key. + * @param keyType + * The type of the key (rsa, ed25519, ...). + * @param key + * The actual key (base 64 encoded). + * @param fingerprint + * The fingerprint of the key. + * @param comment + * An optional comment for the key, quite often this is an email address. + */ +final case class PublicSshKey( + id: UUID, + ownerId: UserId, + keyType: SshKeyType, + key: String, + fingerprint: String, + comment: Option[String] +) diff -rN -u old-smederee/modules/hub/src/test/resources/de/smederee/ssh/ssh-key-with-comment.pub new-smederee/modules/hub/src/test/resources/de/smederee/ssh/ssh-key-with-comment.pub --- old-smederee/modules/hub/src/test/resources/de/smederee/ssh/ssh-key-with-comment.pub 1970-01-01 00:00:00.000000000 +0000 +++ new-smederee/modules/hub/src/test/resources/de/smederee/ssh/ssh-key-with-comment.pub 2025-02-02 04:05:36.566634932 +0000 @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH8ZU2xquZvstbesPktthwY2r5sanULBQKuM5bGHVdeP Some optional comment... diff -rN -u old-smederee/modules/hub/src/test/resources/de/smederee/ssh/ssh-key-without-comment.pub new-smederee/modules/hub/src/test/resources/de/smederee/ssh/ssh-key-without-comment.pub --- old-smederee/modules/hub/src/test/resources/de/smederee/ssh/ssh-key-without-comment.pub 1970-01-01 00:00:00.000000000 +0000 +++ new-smederee/modules/hub/src/test/resources/de/smederee/ssh/ssh-key-without-comment.pub 2025-02-02 04:05:36.566634932 +0000 @@ -0,0 +1 @@ +ssh-dss AAAAB3NzaC1kc3MAAACBAKn1DHh6DaIg/cN6vNVh1VXvHhH86eKelfsolIfvTPQSb3vkqoWPG3T3DGmrUjbqvrrfzaKILTBRv05KqMCbJKETGR0fuY7G1/Nkd/6dZjw1ngYkGd0fr2ERGuq87+gdd1A3TeIqvdjnl7MG3bEGf+fIEJOrRJraZ+u/tDFlSYq/AAAAFQCAUrv94uu1dVTTiyoagKV4Y4QWuQAAAIAuR5mFFYAgT1+t1u16eRCou1nPO4+q35/6uNNCyXtP0BmZaxXqQw25foJz5OzSQWXjjianfRfUyjsHt5DgM0PAIZaqmxMUiVw7BT7zUTa7ucl9NQmFBexiedCtokVb8++vHVZ7Y42tf2CpqVW8T2lw5b8sWb7rHYGarI935qv2bgAAAIABfRnu0PkvysY6QJhUCD4ZKt3qZ6E1cYDivLhDb4GAZxmxSeN5cFPXU3Gst0oNmNjUW55rsZwZP+KkXi3NwAsTd9dZBxkcc+28m8Dr4hGtPTnPp+4p8wzw/X6Lmyr6RSykCK6xuv9rc2td+1fgNyPoWwcLZZQclDj+OdgQVHWj3A== diff -rN -u old-smederee/modules/hub/src/test/resources/de/smederee/ssh/test-ssh-dsa.pub new-smederee/modules/hub/src/test/resources/de/smederee/ssh/test-ssh-dsa.pub --- old-smederee/modules/hub/src/test/resources/de/smederee/ssh/test-ssh-dsa.pub 1970-01-01 00:00:00.000000000 +0000 +++ new-smederee/modules/hub/src/test/resources/de/smederee/ssh/test-ssh-dsa.pub 2025-02-02 04:05:36.566634932 +0000 @@ -0,0 +1 @@ +ssh-dss AAAAB3NzaC1kc3MAAACBALzQVYAbt4a9tfNwqg741R8qlqbwU3ZgJ5BBccOeFqr8FCWTUUp7azKAgoz7FeaDOuRJinbrWhPYo33RQpmKsA/Sq71mq42qA3hRYWUYJOdMjCyvqy2eosLrpEspk5jNn6qB4GabJbLtEfJGink3uucZd472TwmAZzzI4T3c6dOrAAAAFQDQtwJHzk6co5ghipxec1kDhlvH9wAAAIEAo4UKmBnJ8DL2br23FsaLTKshNXNGj4taA5IUvbn89iVN5m9O9IfV7Y2i8gh2sbxfhU9AiXd4ABrg3FcXhWfxXI2geI/uc1mKLePlrf9Bjbb9nLnRO8arQBnXkeVb2weNG/PqzyC3PhPCeDTUSkjFK17V8ljhwhn/vcHeLbkl9rgAAACBAJmpzvFNUKR2ARN4cMbcZ1DMPfw6FmtBlOI1aOtlfTfrfObHL5nwFfwtgOoZQ/TA5/dsfJwjbrC/86orLPuiTynLOhcMdD48tervh/6gKzfrQZskAHxDTi0JttDU7KGacmL26Xm5c27qHni5PkQzCbLPXI2pFWPCBkr8fNb32HmV This is a test key. diff -rN -u old-smederee/modules/hub/src/test/resources/de/smederee/ssh/test-ssh-ecdsa.pub new-smederee/modules/hub/src/test/resources/de/smederee/ssh/test-ssh-ecdsa.pub --- old-smederee/modules/hub/src/test/resources/de/smederee/ssh/test-ssh-ecdsa.pub 1970-01-01 00:00:00.000000000 +0000 +++ new-smederee/modules/hub/src/test/resources/de/smederee/ssh/test-ssh-ecdsa.pub 2025-02-02 04:05:36.566634932 +0000 @@ -0,0 +1 @@ +ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBBv+4uZH1eefZQ0b1eWb8rduh6G4QMvRh7M9hf39Mzj53LtLD2g6ZmhGn0EmwzgV1gVAvgrG8d969/mF57X2bn8= This is a test key. diff -rN -u old-smederee/modules/hub/src/test/resources/de/smederee/ssh/test-ssh-ed25519.pub new-smederee/modules/hub/src/test/resources/de/smederee/ssh/test-ssh-ed25519.pub --- old-smederee/modules/hub/src/test/resources/de/smederee/ssh/test-ssh-ed25519.pub 1970-01-01 00:00:00.000000000 +0000 +++ new-smederee/modules/hub/src/test/resources/de/smederee/ssh/test-ssh-ed25519.pub 2025-02-02 04:05:36.566634932 +0000 @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINxizqtw+eIJrou8QKSJevTh+Zpbp0OQPHZUpVNbCOJM This is a test key. diff -rN -u old-smederee/modules/hub/src/test/resources/de/smederee/ssh/test-ssh-rsa.pub new-smederee/modules/hub/src/test/resources/de/smederee/ssh/test-ssh-rsa.pub --- old-smederee/modules/hub/src/test/resources/de/smederee/ssh/test-ssh-rsa.pub 1970-01-01 00:00:00.000000000 +0000 +++ new-smederee/modules/hub/src/test/resources/de/smederee/ssh/test-ssh-rsa.pub 2025-02-02 04:05:36.566634932 +0000 @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCm9QvkgYHm3eIDLbMgfvEtre3ByAaTwOoDv+8kqGjBqJTU8JsL1LhQndIloiLzAfwTvXfR02u2yvNCTdvy3iiaB90eEO6O2UgRudNHro++Lhqgkwn0aVjha4cjtX8kjszAeCqIsx42Z+r1NwA3wSjQe0ytKZn95dkLGkTVaMqfHaQXjYfqP5vNBugQcbYQv2m/wVzqSjJC9uW/3w7C5hXJK78I+33S86MaTK9g1RSp2zakgokLE7XBWq/gX4CcG0EAWnstoSbjsIQ+ODv1F4V3D8gzTSszKV4EzB95PeOW2XT80RPnljhGCSkO1qo/HrrvpMhWGIDYI3e1i+P9EKmpMsi7FNJNWGLVh+d+DTFWs0WPlqRKzjj+87vrHMcd9W3q1vfnuQRp7RMuEm1r2IUcqsThUqeHEQwKnBMFxnE1MiqZwNdIEodikUoETB0BuD1Shj5FCu18KOAqowN2/NJ1HRllnVNd457h68HwQOLY8A8q51CrjVODZ4FIeTxdsHU= This is a test key. diff -rN -u old-smederee/modules/hub/src/test/scala/de/smederee/ssh/PublicSshKeyTest.scala new-smederee/modules/hub/src/test/scala/de/smederee/ssh/PublicSshKeyTest.scala --- old-smederee/modules/hub/src/test/scala/de/smederee/ssh/PublicSshKeyTest.scala 1970-01-01 00:00:00.000000000 +0000 +++ new-smederee/modules/hub/src/test/scala/de/smederee/ssh/PublicSshKeyTest.scala 2025-02-02 04:05:36.566634932 +0000 @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2022 Contributors as noted in the AUTHORS.md file + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package de.smederee.ssh + +import munit._ + +final class PublicSshKeyTest extends FunSuite { + test("SshKeyString must work for keys with comment") { + val input = scala.io.Source + .fromInputStream( + getClass().getClassLoader().getResourceAsStream("de/smederee/ssh/ssh-key-with-comment.pub"), + "UTF-8" + ) + .getLines() + .mkString + val expected = SshPublicKeyString(input) + SshPublicKeyString.from(input) match { + case None => fail("Key could not be parsed!") + case Some(keyString) => assertEquals(keyString, expected) + } + } + + test("SshKeyString must work for keys without comment") { + val input = scala.io.Source + .fromInputStream( + getClass().getClassLoader().getResourceAsStream("de/smederee/ssh/ssh-key-without-comment.pub"), + "UTF-8" + ) + .getLines() + .mkString + val expected = SshPublicKeyString(input) + SshPublicKeyString.from(input) match { + case None => fail("Key could not be parsed!") + case Some(keyString) => assertEquals(keyString, expected) + } + } +} diff -rN -u old-smederee/modules/hub/src/test/scala/de/smederee/ssh/SshKeyTypeTest.scala new-smederee/modules/hub/src/test/scala/de/smederee/ssh/SshKeyTypeTest.scala --- old-smederee/modules/hub/src/test/scala/de/smederee/ssh/SshKeyTypeTest.scala 1970-01-01 00:00:00.000000000 +0000 +++ new-smederee/modules/hub/src/test/scala/de/smederee/ssh/SshKeyTypeTest.scala 2025-02-02 04:05:36.566634932 +0000 @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2022 Contributors as noted in the AUTHORS.md file + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package de.smederee.ssh + +import munit._ + +final class SshKeyTypeTest extends FunSuite { + test("from must work for ssh-dsa") { + val input = scala.io.Source + .fromInputStream(getClass().getClassLoader().getResourceAsStream("de/smederee/ssh/test-ssh-dsa.pub"), "UTF-8") + .getLines() + .mkString + assertEquals( + SshKeyType.from(input), + Some(SshKeyType.SshDsa), + s"Incorrect key type extracted from: $input" + ) + } + + test("from must work for ssh-ecdsa") { + val input = scala.io.Source + .fromInputStream(getClass().getClassLoader().getResourceAsStream("de/smederee/ssh/test-ssh-ecdsa.pub"), "UTF-8") + .getLines() + .mkString + assertEquals( + SshKeyType.from(input), + Some(SshKeyType.SshEcDsa), + s"Incorrect key type extracted from: $input" + ) + } + + test("from must work for ssh-ed25519") { + val input = scala.io.Source + .fromInputStream(getClass().getClassLoader().getResourceAsStream("de/smederee/ssh/test-ssh-ed25519.pub"), "UTF-8") + .getLines() + .mkString + assertEquals( + SshKeyType.from(input), + Some(SshKeyType.SshEd25519), + s"Incorrect key type extracted from: $input" + ) + } + + test("from must work for ssh-rsa") { + val input = scala.io.Source + .fromInputStream(getClass().getClassLoader().getResourceAsStream("de/smederee/ssh/test-ssh-rsa.pub"), "UTF-8") + .getLines() + .mkString + assertEquals( + SshKeyType.from(input), + Some(SshKeyType.SshRsa), + s"Incorrect key type extracted from: $input" + ) + } + +}