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
+
+
+
+ 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)