From e2c65a044030aeb780c70ebb73a29a8419c22bdf Mon Sep 17 00:00:00 2001 From: "Fausto M. Lagos S" Date: Tue, 26 Jul 2022 10:02:02 -0500 Subject: [PATCH] #54 - Adding functionality to show the SHA-1 fingerprint in the key details to public keys and key pairs. --- api/key_entry.go | 5 ++ gui/definitions/interface/KeyDetails.xml | 47 +++++++++++++ gui/definitions/styles/global.css | 4 ++ gui/key_details.go | 68 +++++++++++++++--- gui/key_details_test.go | 88 ++++++++++++++++++++++++ gui/key_list_test.go | 13 ++++ gui/style.go | 25 ------- ssh/api_test.go | 12 ++-- ssh/file_reader.go | 5 +- ssh/file_reader_test.go | 8 +-- ssh/key_reader.go | 2 +- ssh/key_reader_test.go | 23 +++++-- ssh/key_representation.go | 7 +- ssh/key_representation_test.go | 26 ++++++- ssh/public_key_parser.go | 16 ++++- ssh/rsa_file_reader.go | 38 +++++----- ssh/slices.go | 6 ++ 17 files changed, 312 insertions(+), 81 deletions(-) diff --git a/api/key_entry.go b/api/key_entry.go index 30d5df7..642aeb5 100644 --- a/api/key_entry.go +++ b/api/key_entry.go @@ -6,3 +6,8 @@ type KeyEntry interface { PrivateKeyLocations() []string KeyType() KeyType } + +type PublicKeyEntry interface { + KeyEntry + WithDigestContent(func([]byte) []byte) []byte +} diff --git a/gui/definitions/interface/KeyDetails.xml b/gui/definitions/interface/KeyDetails.xml index bca4673..57b1683 100644 --- a/gui/definitions/interface/KeyDetails.xml +++ b/gui/definitions/interface/KeyDetails.xml @@ -113,6 +113,53 @@ 2 + + + True + False + + + True + False + Fingerprint: + + + + False + True + 0 + + + + + True + False + start + end + 20 + True + + + + True + True + 1 + + + + + + False + True + 3 + + diff --git a/gui/definitions/styles/global.css b/gui/definitions/styles/global.css index 0589894..6221600 100644 --- a/gui/definitions/styles/global.css +++ b/gui/definitions/styles/global.css @@ -110,4 +110,8 @@ window { .keyEntry.pair { background-color: @keylist-entry-pair-bg; +} + +.fingerprint .fingerprintLabel { + padding-right: 10px; } \ No newline at end of file diff --git a/gui/key_details.go b/gui/key_details.go index ac93fa1..d3460cb 100644 --- a/gui/key_details.go +++ b/gui/key_details.go @@ -1,8 +1,11 @@ package gui import ( + "crypto/sha1" + "fmt" "github.com/coyim/gotk3adapter/gtki" "github.com/digitalautonomy/keymirror/api" + "strings" ) type clearable[T any] interface { @@ -16,17 +19,6 @@ func clearAllChildrenOf[T any](b clearable[T]) { } } -func (kd *keyDetails) displayLocations(keyLocations []string, pathLabelName, rowName string) { - if keyLocations != nil { - label := kd.builder.get(pathLabelName).(gtki.Label) - label.SetLabel(keyLocations[0]) - label.SetTooltipText(keyLocations[0]) - } else { - row := kd.builder.get(rowName).(gtki.Box) - row.Hide() - } -} - type keyDetails struct { builder *builder key api.KeyEntry @@ -67,9 +59,63 @@ func (kd *keyDetails) setClassForKeyDetails() { addClass(kd.box, className) } +func (kd *keyDetails) displayLocations(keyLocations []string, pathLabelName, rowName string) { + if keyLocations != nil { + label := kd.builder.get(pathLabelName).(gtki.Label) + label.SetLabel(keyLocations[0]) + label.SetTooltipText(keyLocations[0]) + } else { + row := kd.builder.get(rowName).(gtki.Box) + row.Hide() + } +} + +func returningSlice20(f func([]byte) [20]byte) func([]byte) []byte { + return func(v []byte) []byte { + res := f(v) + return res[:] + } +} + +func returningSlice32(f func([]byte) [32]byte) func([]byte) []byte { + return func(v []byte) []byte { + res := f(v) + return res[:] + } +} + +func formatByteForFingerprint(b byte) string { + return fmt.Sprintf("%02X", b) +} + +func formatFingerprint(f []byte) string { + result := []string{} + + for _, v := range f { + result = append(result, formatByteForFingerprint(v)) + } + + return strings.Join(result, ":") +} + +const fingerprintRow string = "keyFingerprintRow" + +func (kd *keyDetails) displayFingerprint(rowName string) { + if pk, ok := kd.key.(api.PublicKeyEntry); ok { + f := formatFingerprint(pk.WithDigestContent(returningSlice20(sha1.Sum))) + label := kd.builder.get("fingerprint").(gtki.Label) + label.SetLabel(f) + label.SetTooltipText(f) + } else { + row := kd.builder.get(rowName).(gtki.Box) + row.Hide() + } +} + func (kd *keyDetails) display() { kd.displayLocations(kd.key.PublicKeyLocations(), publicKeyPathLabel, publicKeyRowName) kd.displayLocations(kd.key.PrivateKeyLocations(), privateKeyPathLabel, privateKeyRowName) + kd.displayFingerprint(fingerprintRow) kd.setClassForKeyDetails() } diff --git a/gui/key_details_test.go b/gui/key_details_test.go index 03aa27f..a089695 100644 --- a/gui/key_details_test.go +++ b/gui/key_details_test.go @@ -32,6 +32,9 @@ func (s *guiSuite) Test_populateKeyDetails_createsTheKeyDetailsBoxAndDisplaysThe privateKeyRow := >k.MockBox{} builder.On("GetObject", "keyDetailsPrivateKeyRow").Return(privateKeyRow, nil).Once() + fingerprintRowMock := >k.MockBox{} + builder.On("GetObject", "keyFingerprintRow").Return(fingerprintRowMock, nil).Once() + keMock := &keyEntryMock{} keMock.On("PublicKeyLocations").Return([]string{"/a/path/to/a/public/key"}).Once() keMock.On("PrivateKeyLocations").Return(nil).Once() @@ -39,6 +42,7 @@ func (s *guiSuite) Test_populateKeyDetails_createsTheKeyDetailsBoxAndDisplaysThe publicKeyPathLabel.On("SetLabel", "/a/path/to/a/public/key").Return().Once() publicKeyPathLabel.On("SetTooltipText", "/a/path/to/a/public/key").Return().Once() privateKeyRow.On("Hide").Return().Once() + fingerprintRowMock.On("Hide").Return().Once() scMock := expectClassToBeAdded(keyDetailsBoxMock, "publicKey") @@ -49,6 +53,7 @@ func (s *guiSuite) Test_populateKeyDetails_createsTheKeyDetailsBoxAndDisplaysThe keMock.AssertExpectations(s.T()) publicKeyPathLabel.AssertExpectations(s.T()) privateKeyRow.AssertExpectations(s.T()) + fingerprintRowMock.AssertExpectations(s.T()) scMock.AssertExpectations(s.T()) } @@ -66,6 +71,9 @@ func (s *guiSuite) Test_populateKeyDetails_createsTheKeyDetailsBoxAndDisplaysThe publicKeyRow := >k.MockBox{} builder.On("GetObject", "keyDetailsPublicKeyRow").Return(publicKeyRow, nil).Once() + fingerprintRowMock := >k.MockBox{} + builder.On("GetObject", "keyFingerprintRow").Return(fingerprintRowMock, nil).Once() + keMock := &keyEntryMock{} keMock.On("PublicKeyLocations").Return(nil).Once() keMock.On("PrivateKeyLocations").Return([]string{"/a/path/to/a/private/key"}).Once() @@ -73,6 +81,7 @@ func (s *guiSuite) Test_populateKeyDetails_createsTheKeyDetailsBoxAndDisplaysThe privateKeyPathLabel.On("SetLabel", "/a/path/to/a/private/key").Return().Once() privateKeyPathLabel.On("SetTooltipText", "/a/path/to/a/private/key").Return().Once() publicKeyRow.On("Hide").Return().Once() + fingerprintRowMock.On("Hide").Return().Once() scMock := expectClassToBeAdded(keyDetailsBoxMock, "privateKey") @@ -83,6 +92,7 @@ func (s *guiSuite) Test_populateKeyDetails_createsTheKeyDetailsBoxAndDisplaysThe keMock.AssertExpectations(s.T()) privateKeyPathLabel.AssertExpectations(s.T()) publicKeyRow.AssertExpectations(s.T()) + fingerprintRowMock.AssertExpectations(s.T()) scMock.AssertExpectations(s.T()) } @@ -100,6 +110,9 @@ func (s *guiSuite) Test_populateKeyDetails_createsTheKeyDetailsBoxAndDisplaysBot privateKeyPathLabel := >k.MockLabel{} builder.On("GetObject", "privateKeyPath").Return(privateKeyPathLabel, nil).Once() + fingerprintRowMock := >k.MockBox{} + builder.On("GetObject", "keyFingerprintRow").Return(fingerprintRowMock, nil).Once() + keMock := &keyEntryMock{} keMock.On("PublicKeyLocations").Return([]string{"/a/path/to/a/public/key"}).Once() keMock.On("PrivateKeyLocations").Return([]string{"/a/path/to/a/private/key"}).Once() @@ -108,6 +121,7 @@ func (s *guiSuite) Test_populateKeyDetails_createsTheKeyDetailsBoxAndDisplaysBot publicKeyPathLabel.On("SetTooltipText", "/a/path/to/a/public/key").Return().Once() privateKeyPathLabel.On("SetLabel", "/a/path/to/a/private/key").Return().Once() privateKeyPathLabel.On("SetTooltipText", "/a/path/to/a/private/key").Return().Once() + fingerprintRowMock.On("Hide").Return().Once() scMock := expectClassToBeAdded(keyDetailsBoxMock, "keyPair") @@ -118,6 +132,7 @@ func (s *guiSuite) Test_populateKeyDetails_createsTheKeyDetailsBoxAndDisplaysBot keMock.AssertExpectations(s.T()) publicKeyPathLabel.AssertExpectations(s.T()) privateKeyPathLabel.AssertExpectations(s.T()) + fingerprintRowMock.AssertExpectations(s.T()) scMock.AssertExpectations(s.T()) } @@ -134,3 +149,76 @@ func (s *guiSuite) Test_clearAllChildrenOf_removeEachOneOfTheChildrenOfTheBox() boxMock.AssertExpectations(s.T()) } + +func (s *guiSuite) Test_formatFingerprint_returnsAnUpperCaseHexadecimalStringWithColons() { + f := []byte{} + expected := "" + s.Equal(expected, formatFingerprint(f)) + + f = []byte{0} + expected = "00" + s.Equal(expected, formatFingerprint(f)) + + f = []byte{8} + expected = "08" + s.Equal(expected, formatFingerprint(f)) + + f = []byte{0xfe} + expected = "FE" + s.Equal(expected, formatFingerprint(f)) + + f = []byte{0, 1, 32, 0x67, 0, 7, 0xfc, 0} + expected = "00:01:20:67:00:07:FC:00" + s.Equal(expected, formatFingerprint(f)) +} + +func (s *guiSuite) Test_keyDetails_displayFingerprint_calculateTheFingerprintAndDisplaysIt() { + keyMock := &publicKeyEntryMock{} + var calledWithFunc *func([]byte) []byte + keyMock.On("WithDigestContent", mock.AnythingOfType("func([]uint8) []uint8")).Return( + []byte("something")).Run(func(a mock.Arguments) { + ff := a.Get(0).(func([]byte) []byte) + calledWithFunc = &ff + }) + + builderMock := >k.MockBuilder{} + + kd := &keyDetails{ + builder: &builder{builderMock}, + key: keyMock, + } + + labelMock := >k.MockLabel{} + builderMock.On("GetObject", "fingerprint").Return(labelMock, nil).Once() + labelMock.On("SetLabel", "73:6F:6D:65:74:68:69:6E:67").Return().Once() + labelMock.On("SetTooltipText", "73:6F:6D:65:74:68:69:6E:67").Return().Once() + + kd.displayFingerprint("a row") + + labelMock.AssertExpectations(s.T()) + builderMock.AssertExpectations(s.T()) + keyMock.AssertExpectations(s.T()) + + s.NotNil(calledWithFunc) + s.Equal([]byte{0x2a, 0xae, 0x6c, 0x35, 0xc9, 0x4f, 0xcf, 0xb4, 0x15, 0xdb, 0xe9, 0x5f, 0x40, 0x8b, 0x9c, 0xe9, 0x1e, 0xe8, 0x46, 0xed}, (*calledWithFunc)([]byte("hello world"))) + s.Equal([]byte{0x0, 0x78, 0xbb, 0x8e, 0x5c, 0x9d, 0x8a, 0xbf, 0x7f, 0x1e, 0x4e, 0x14, 0xc8, 0x7d, 0x90, 0x23, 0x23, 0x5b, 0x62, 0x30}, (*calledWithFunc)([]byte("goodbye world"))) +} + +func (s *guiSuite) Test_keyDetails_displayFingerprint_hideTheFingerprintRow_ifDisplayAPrivateKey() { + keyMock := &keyEntryMock{} + + builderMock := >k.MockBuilder{} + rowMock := >k.MockBox{} + + kd := &keyDetails{ + builder: &builder{builderMock}, + key: keyMock, + } + + rowMock.On("Hide").Return().Once() + + builderMock.On("GetObject", "label").Return(rowMock, nil).Once() + kd.displayFingerprint("label") + + rowMock.AssertExpectations(s.T()) +} diff --git a/gui/key_list_test.go b/gui/key_list_test.go index 7ab9836..5495dd6 100644 --- a/gui/key_list_test.go +++ b/gui/key_list_test.go @@ -45,6 +45,15 @@ func (ke *keyEntryMock) KeyType() api.KeyType { return ret[api.KeyType](returns, 0) } +type publicKeyEntryMock struct { + keyEntryMock +} + +func (pk *publicKeyEntryMock) WithDigestContent(f func([]byte) []byte) []byte { + returns := pk.Called(f) + return ret[[]byte](returns, 0) +} + func (s *guiSuite) Test_createKeyEntryBoxFrom_CreatesAGTKIBoxWithTheGivenASSHKeyEntry() { box := s.setupBuildingOfKeyEntry("/home/amnesia/id_rsa.pub") @@ -86,6 +95,9 @@ func (s *guiSuite) Test_createKeyEntryBoxFrom_CreatesAGTKIBoxWithTheGivenASSHKey detailsRevMock.On("Show").Return().Once() detailsRevMock.On("SetRevealChild", true).Return().Once() privateKeyRow.On("Hide").Return().Once() + fingerprintRowMock := >k.MockBox{} + builder.On("GetObject", "keyFingerprintRow").Return(fingerprintRowMock, nil).Once() + fingerprintRowMock.On("Hide").Return().Once() scMock1 := expectClassToBeAdded(box, "current") scMock2 := expectClassToBeAdded(keyDetailsBoxMock, "publicKey") @@ -100,6 +112,7 @@ func (s *guiSuite) Test_createKeyEntryBoxFrom_CreatesAGTKIBoxWithTheGivenASSHKey scMock1.AssertExpectations(s.T()) scMock2.AssertExpectations(s.T()) privateKeyRow.AssertExpectations(s.T()) + fingerprintRowMock.AssertExpectations(s.T()) } type keyAccessMock struct { diff --git a/gui/style.go b/gui/style.go index fd64a76..92057a6 100644 --- a/gui/style.go +++ b/gui/style.go @@ -5,31 +5,6 @@ import ( "io/fs" ) -// -//// #cgo pkg-config: gdk-3.0 gio-2.0 glib-2.0 gobject-2.0 gtk+-3.0 -//// #include -//// #include -//import "C" -// -//func withDefinitionInTempFile(def string, f func(string)) { -// content, _ := fs.ReadFile(getDefinitions(), def) -// td, _ := ioutil.TempDir("", "") -// defer func() { -// _ = os.RemoveAll(td) -// }() -// file := path.Join(td, path.Base(def)) -// _ = ioutil.WriteFile(file, content, 0755) -// f(file) -//} -// -//func testBla() { -// withDefinitionInTempFile("definitions/icons.gresource", func(path string) { -// gr, _ := gio.LoadGResource(path) -// gio.RegisterGResource(gr) -// C.gtk_icon_theme_add_resource_path(C.gtk_icon_theme_get_default(), C.CString("/digital/autonomia/KeyMirror")) -// }) -//} - func (u *ui) createStyleProviderFrom(filename string) gtki.CssProvider { cssProvider, _ := u.gtk.CssProviderNew() pathOfFile := styleDefinitionPath(filename) diff --git a/ssh/api_test.go b/ssh/api_test.go index a4248c0..76c70b0 100644 --- a/ssh/api_test.go +++ b/ssh/api_test.go @@ -73,9 +73,11 @@ func (s *sshSuite) Test_access_AllKeys_ReturnsAKeyEntryListOfPublicKeysIfSSHDire publicKeyFile1 := "ssh-rsa.pub" publicKeyFile2 := fmt.Sprintf("ssh-rsa-%d.pub", r) publicKeyFile3 := "ed25519.pub" + publicKeyFile4 := "other_ed25519.pub" s.createFileWithContent(sshDirectory, publicKeyFile1, "ssh-rsa BBBB batman@debian") s.createFileWithContent(sshDirectory, publicKeyFile2, "ssh-rsa AAAA robin@debian") - s.createFileWithContent(sshDirectory, publicKeyFile3, "ssh-ed25519 CCC alfred@debian") + s.createFileWithContent(sshDirectory, publicKeyFile3, "ssh-ed25519 CCCC alfred@debian") + s.createFileWithContent(sshDirectory, publicKeyFile4, "ssh-ed25519 DDD penguin@debian") s.createEmptyFile(sshDirectory, "empty-file") a, _ := accessWithTestLogging() @@ -90,7 +92,7 @@ func (s *sshSuite) Test_access_AllKeys_ReturnsAKeyEntryListOfPublicKeysIfSSHDire func createPublicKeyRepresentationForTest(path, key string) *publicKeyRepresentation { return createPublicKeyRepresentationFromPublicKey(&publicKey{ location: path, - key: key, + key: decode(key), }) } @@ -121,7 +123,7 @@ func (s *sshSuite) Test_access_AllKeys_ReturnsAKeyEntryListOfKeypairsIfSSHDirect createKeypairRepresentation(createPrivateKeyRepresentation(path.Join(sshDirectory, matchingPrivateKeyFile2)), createPublicKeyRepresentationForTest(path.Join(sshDirectory, matchingPublicKeyFile2), "AAAA")), createKeypairRepresentation(createPrivateKeyRepresentation(path.Join(sshDirectory, matchingPrivateKeyFile3)), - createPublicKeyRepresentation(path.Join(sshDirectory, matchingPublicKeyFile3))), + createPublicKeyRepresentationForTest(path.Join(sshDirectory, matchingPublicKeyFile3), "CCCC")), }, a.AllKeys()) } @@ -147,13 +149,13 @@ func (s *sshSuite) Test_access_AllKeys_ReturnsAKeyEntryListIfSSHDirectoryPublicA createPublicKeyRepresentationFromPublicKey( &publicKey{ location: path.Join(sshDirectory, lonelyPublicKeyFile), - key: "AAAA", + key: decode("AAAA"), }), createKeypairRepresentation(createPrivateKeyRepresentation(path.Join(sshDirectory, matchingPrivateKey)), createPublicKeyRepresentationFromPublicKey( &publicKey{ location: path.Join(sshDirectory, matchingPublicKey), - key: "BBBB", + key: decode("BBBB"), }), ), }, a.AllKeys()) diff --git a/ssh/file_reader.go b/ssh/file_reader.go index 72a45bb..592175c 100644 --- a/ssh/file_reader.go +++ b/ssh/file_reader.go @@ -58,9 +58,10 @@ func publicKeyRepresentationsFrom(input []string) []*publicKeyRepresentation { rsaKeys := rsaPublicKeysFrom(input) rsaKeyRepresentations := createPublicKeyRepresentationsFromPublicKeys(rsaKeys) - publicKeyFiles := filesContainingEd25519PublicKeys(input) + ed25519Keys := ed25519KeyFrom(input) + ed25519KeyRepresentations := createPublicKeyRepresentationsFromPublicKeys(ed25519Keys) return concat( rsaKeyRepresentations, - createPublicKeyRepresentationsFrom(publicKeyFiles)) + ed25519KeyRepresentations) } diff --git a/ssh/file_reader_test.go b/ssh/file_reader_test.go index de1655b..7e7f0ed 100644 --- a/ssh/file_reader_test.go +++ b/ssh/file_reader_test.go @@ -50,7 +50,7 @@ func (s *sshSuite) Test_checkIfFileContainsAPublicRSAKey_ReturnsFalseWhenTheFile func (s *sshSuite) Test_checkIfFileContainsAPublicRSAKey_ReturnsTrueWhenTheFileContentIsAnRSAPublicKey() { fileName := "a file with a valid RSA Public Key" - s.createFileWithContent(s.tdir, fileName, "ssh-rsa AAAAA batman@debian") + s.createFileWithContent(s.tdir, fileName, "ssh-rsa b3RoZXIgdmFsaWQgc3RyaW5n batman@debian") b, err := checkIfFileContainsAPublicRSAKey(filepath.Join(s.tdir, fileName)) @@ -103,7 +103,7 @@ func (s *sshSuite) Test_selectFilesContainingRSAPublicKeys_ReturnsAnEmptyListIfA func (s *sshSuite) Test_selectFilesContainingRSAPublicKeys_ReturnsAListWithOneFileNameIfAListWithAFileThatContainsAnRSAPublicKeyIsProvided() { // Given fileName := "File-with-content" - s.createFileWithContent(s.tdir, fileName, "ssh-rsa AAAAA batman@debian") + s.createFileWithContent(s.tdir, fileName, "ssh-rsa b3RoZXIgdmFsaWQgc3RyaW5n batman@debian") fileNameList := []string{filepath.Join(s.tdir, fileName)} // When @@ -454,10 +454,6 @@ func (s *sshSuite) Test_publicKeyEntriesFrom_ReturnsAListOfPublicKeyEntriesFromA filepath.Join(s.tdir, publicRSAKeyFile1), filepath.Join(s.tdir, publicRSAKeyFile2), }), l) - /*s.Equal([]*publicKeyRepresentation{ - createPublicKeyRepresentation(filepath.Join(s.tdir, publicRSAKeyFile1)), - createPublicKeyRepresentation(filepath.Join(s.tdir, publicRSAKeyFile2)), - }, l)*/ } func (s *sshSuite) Test_partitionKeyEntries_ReturnsAListOfKeyEntriesWithPublicPrivateAndKeyPairsFromPublicAndPrivateKeyRepresentations() { diff --git a/ssh/key_reader.go b/ssh/key_reader.go index d5e7463..5955bd8 100644 --- a/ssh/key_reader.go +++ b/ssh/key_reader.go @@ -14,7 +14,7 @@ func listFilesIn(dir string) []string { type publicKey struct { location string algorithm string - key string + key []byte comment string } diff --git a/ssh/key_reader_test.go b/ssh/key_reader_test.go index 6d54264..369f397 100644 --- a/ssh/key_reader_test.go +++ b/ssh/key_reader_test.go @@ -42,7 +42,7 @@ func (s *sshSuite) Test_ParseAStringAsAnSSHPublicKeyRepresentation() { _, ok := parsePublicKey(k) s.Require().False(ok, "An empty string is not a valid SSH public key representation") - k = "ssh-rsa bla batman@debian" + k = "ssh-rsa dmFsaWQgYmFzZTY0IHN0cmluZw== batman@debian" pub, ok := parsePublicKey(k) s.Require().True(ok, "Should parse a valid SSH RSA public key representation") s.Equal("ssh-rsa", pub.algorithm) @@ -60,18 +60,18 @@ func (s *sshSuite) Test_ParseAStringAsAnSSHPublicKeyRepresentation() { _, ok = parsePublicKey(k) s.False(ok, "Since more than one whitespace character serve as one single separator, this example only has one column, and is thus not valid") - k = "ssh-rsa AAAAA foo@debian" + k = "ssh-rsa b3RoZXIgdmFsaWQgc3RyaW5n foo@debian" _, ok = parsePublicKey(k) s.True(ok, "More than one whitespace character serves as one single separator between columns") - k = "ssh-rsa\tAAAAA foo@debian" + k = "ssh-rsa\tb3RoZXIgdmFsaWQgc3RyaW5n foo@debian" _, ok = parsePublicKey(k) s.True(ok, "A tab can be a separator for columns") - k = "ssh-rsa \t \t \t AAAAA foo@debian" + k = "ssh-rsa \t \t \t b3RoZXIgdmFsaWQgc3RyaW5n foo@debian" pub, ok = parsePublicKey(k) s.True(ok, "A mix of tabs and spaces serve as one single separator") - s.Equal("AAAAA", pub.key) + s.Equal(decode("b3RoZXIgdmFsaWQgc3RyaW5n"), pub.key) k = "ssh-rsa AAQQ foo@debian foo2@debian" pub, ok = parsePublicKey(k) @@ -83,6 +83,19 @@ func (s *sshSuite) Test_ParseAStringAsAnSSHPublicKeyRepresentation() { s.True(ok, "An SSH public key without a comment is still acceptable") } +func (s *sshSuite) Test_parsePublicKey_doesNotParseAKeyThatIsNotAValidBase64() { + k := "ssh-rsa b batman@debian" + _, ok := parsePublicKey(k) + s.Require().False(ok, "Should not accept a non-base64 key") +} + +func (s *sshSuite) Test_parsePublicKey_parsesABase64Key() { + k := "ssh-rsa YSB2YWxpZCBiYXNlNjQga2V5 batman@debian" + key, ok := parsePublicKey(k) + s.Require().True(ok, "Should accept a Base64 key") + s.Require().Equal([]byte("a valid base64 key"), key.key) +} + func (s *sshSuite) Test_CheckIfThePublicKeyTypeIdentifierIsRSA() { pub := publicKey{} s.False(pub.isRSA(), "An empty key is not an RSA key") diff --git a/ssh/key_representation.go b/ssh/key_representation.go index a6e9b05..1f2e6ff 100644 --- a/ssh/key_representation.go +++ b/ssh/key_representation.go @@ -27,8 +27,7 @@ func createPublicKeyRepresentation(path string) *publicKeyRepresentation { func createPublicKeyRepresentationFromPublicKey(key *publicKey) *publicKeyRepresentation { return &publicKeyRepresentation{ path: key.location, - // TODO: this should decode the base64 before setting it - key: []byte(key.key), + key: key.key, } } @@ -118,3 +117,7 @@ func (k *keypairRepresentation) PublicKeyLocations() []string { func (k *keypairRepresentation) KeyType() api.KeyType { return api.PairKeyType } + +func (k *keypairRepresentation) WithDigestContent(f func([]byte) []byte) []byte { + return k.public.WithDigestContent(f) +} diff --git a/ssh/key_representation_test.go b/ssh/key_representation_test.go index b2f41fb..eea956e 100644 --- a/ssh/key_representation_test.go +++ b/ssh/key_representation_test.go @@ -285,9 +285,7 @@ const otherKey = "AAAAB3NzaC1yc2EAAAADAQABAAABgQC6CyfdeOltbKbISAuuvH27pLNxsNsJ18 func (s *sshSuite) Test_publicKeyRepresentation_WithDigestContent_returnsTheFingerPrint() { key := &publicKeyRepresentation{key: decode(originalKey)} fingerprint := key.key - result := key.WithDigestContent(func(in []byte) []byte { - return in - }) + result := key.WithDigestContent(identity[[]byte]) s.Equal(result, fingerprint) key = &publicKeyRepresentation{key: decode(originalKey)} @@ -306,3 +304,25 @@ func (s *sshSuite) Test_publicKeyRepresentation_WithDigestContent_returnsTheFing }) s.Equal(result, fingerprint) } + +func identity[T any](v T) T { + return v +} + +func (s *sshSuite) Test_keypairRepresentation_WithDigestContent_returnsTheFingerPrint() { + keyPair := &keypairRepresentation{ + public: &publicKeyRepresentation{key: decode(originalKey)}, + } + + s.Equal( + keyPair.WithDigestContent(identity[[]byte]), + keyPair.public.WithDigestContent(identity[[]byte])) + + keyPair = &keypairRepresentation{ + public: &publicKeyRepresentation{key: decode(otherKey)}, + } + + s.Equal( + keyPair.WithDigestContent(identity[[]byte]), + keyPair.public.WithDigestContent(identity[[]byte])) +} diff --git a/ssh/public_key_parser.go b/ssh/public_key_parser.go index fc13975..36b56f5 100644 --- a/ssh/public_key_parser.go +++ b/ssh/public_key_parser.go @@ -1,6 +1,7 @@ package ssh import ( + "encoding/base64" "regexp" "strings" ) @@ -26,9 +27,14 @@ func (p *publicKeyParser) parse() (publicKey, bool) { return publicKey{}, false } + key, ok := p.parseKey() + if !ok { + return publicKey{}, false + } + return publicKey{ algorithm: p.algorithm(), - key: p.key(), + key: key, comment: p.potentialComment(), }, true } @@ -36,12 +42,16 @@ func (p *publicKeyParser) parse() (publicKey, bool) { func (p *publicKeyParser) notEnoughFields() bool { return len(p.fields) == 1 } + func (p *publicKeyParser) algorithm() string { return p.fields[0] } -func (p *publicKeyParser) key() string { - return p.fields[1] + +func (p *publicKeyParser) parseKey() ([]byte, bool) { + k, e := base64.StdEncoding.DecodeString(p.fields[1]) + return k, e == nil } + func (p *publicKeyParser) potentialComment() string { if p.hasComment() { return p.fields[2] diff --git a/ssh/rsa_file_reader.go b/ssh/rsa_file_reader.go index d61791d..56e7a74 100644 --- a/ssh/rsa_file_reader.go +++ b/ssh/rsa_file_reader.go @@ -14,25 +14,27 @@ func filesContainingRSAPublicKeys(fileNameList []string) []string { return filter(fileNameList, ignoringErrors(checkIfFileContainsAPublicRSAKey)) } +func publicKeyFromFile(fileName string) *publicKey { + content, e := os.ReadFile(fileName) + if e != nil { + return nil + } + + pub, ok := parsePublicKey(string(content)) + if !ok { + return nil + } + + pub.location = fileName + return &pub +} + func rsaPublicKeysFrom(fileNameList []string) []*publicKey { - return filter(transform(fileNameList, func(fileName string) *publicKey { - content, e := os.ReadFile(fileName) - if e != nil { - return nil - } - - pub, ok := parsePublicKey(string(content)) - if !ok { - return nil - } - pub.location = fileName - return &pub - }), func(pub *publicKey) bool { - if pub == nil { - return false - } - return pub.isRSA() - }) + return filter(transform(fileNameList, publicKeyFromFile), both(not(isNil[publicKey]), (*publicKey).isRSA)) +} + +func ed25519KeyFrom(fileNameList []string) []*publicKey { + return filter(transform(fileNameList, publicKeyFromFile), both(not(isNil[publicKey]), (*publicKey).isEd25519)) } func (a *access) filesContainingRSAPrivateKeys(fileNameList []string) []string { diff --git a/ssh/slices.go b/ssh/slices.go index 7a30429..d6171b1 100644 --- a/ssh/slices.go +++ b/ssh/slices.go @@ -75,6 +75,12 @@ func not[T any](f predicate[T]) predicate[T] { } } +func both[T any](f, f2 predicate[T]) predicate[T] { + return func(v T) bool { + return f(v) && f2(v) + } +} + func loggingErrors[T, R any](l logrus.FieldLogger, message string, f func(T) (R, error)) func(T) R { return func(s T) R { b, e := f(s)