25#include <boost/uuid/detail/sha1.hpp>
26#include <boost/version.hpp>
46#include <wx/filename.h>
49#include <wx/tokenzr.h>
55std::array<uint8_t, 20> sha1(
const std::string& aData )
57 boost::uuids::detail::sha1 hasher;
58 hasher.process_bytes( aData.data(), aData.size() );
60 std::array<uint8_t, 20>
result;
63#if BOOST_VERSION >= 108600
64 unsigned char digest[20];
65 hasher.get_digest( digest );
66 std::copy( digest, digest + 20,
result.begin() );
68 unsigned int digest[5];
69 hasher.get_digest( digest );
71 for(
size_t ii = 0; ii < 5; ++ii )
73 for(
size_t jj = 0; jj < 4; ++jj )
74 result[ii * 4 + jj] =
static_cast<uint8_t
>( digest[ii] >> ( 24 - 8 * jj ) );
82std::string decodeBase64(
const wxString& aText )
85 wxMemoryBuffer buffer = wxBase64Decode( aText, wxBase64DecodeMode_Strict, &errorPos );
87 return std::string(
static_cast<const char*
>( buffer.GetData() ), buffer.GetDataLen() );
91wxString hostToken(
const wxString& aHost,
int aPort )
93 if( aPort <= 0 || aPort == 22 )
96 return wxString::Format( wxS(
"[%s]:%d" ), aHost.Lower(), aPort );
101bool hostsFieldMatches(
const wxString& aField,
const wxString& aToken )
103 if( aField.StartsWith( wxS(
"|1|" ) ) )
105 wxArrayString parts = wxSplit( aField.Mid( 3 ),
'|',
'\0' );
107 if( parts.size() != 2 )
110 std::array<uint8_t, 20> mac =
KIGIT::HmacSha1( decodeBase64( parts[0] ), aToken.utf8_string() );
112 return decodeBase64( parts[1] ) == std::string( mac.begin(), mac.end() );
115 bool matched =
false;
117 for(
const wxString& pattern : wxSplit( aField.Lower(),
',',
'\0' ) )
119 const bool negated = pattern.StartsWith( wxS(
"!" ) );
121 if( !aToken.Matches( negated ? pattern.Mid( 1 ) : pattern ) )
135wxString firstExistingDir(
const wxString& aList )
139 for(
size_t ii = 0; ii <= aList.size(); ++ii )
141 if( ii < aList.size() && ( aList[ii] != GIT_PATH_LIST_SEPARATOR || ( ii > 0 && aList[ii - 1] ==
'\\' ) ) )
143 current += aList[ii];
147 if( !current.IsEmpty() && wxFileName::DirExists( current ) )
153 return wxEmptyString;
158bool appendToFile(
const wxString& aPath,
const std::string& aText )
161 std::ofstream out( aPath.fn_str(), std::ios::binary | std::ios::app );
167 struct FILE_DESCRIPTOR
176 } file{ open( aPath.fn_str(), O_WRONLY | O_APPEND | O_CREAT | O_CLOEXEC, 0600 ) };
181 for(
size_t written = 0; written < aText.size(); )
183 ssize_t rc = write( file.fd, aText.data() + written, aText.size() - written );
185 if( rc < 0 && errno == EINTR )
191 written +=
static_cast<size_t>( rc );
196 return close( fd ) == 0;
202bool endsWithNewline(
const wxString& aPath )
204 if( !wxFileName::FileExists( aPath ) )
207 std::ifstream in( aPath.fn_str(), std::ios::binary | std::ios::ate );
212 std::streamoff size = in.tellg();
218 in.seekg( size - 1 );
221 return in && last ==
'\n';
227std::array<uint8_t, 20>
KIGIT::HmacSha1(
const std::string& aKey,
const std::string& aData )
229 constexpr size_t blockSize = 64;
231 std::string key = aKey;
233 if( key.size() > blockSize )
235 std::array<uint8_t, 20> hashed = sha1( key );
236 key.assign( hashed.begin(), hashed.end() );
239 key.resize( blockSize,
'\0' );
241 std::string inner( blockSize,
'\0' );
242 std::string outer( blockSize,
'\0' );
244 for(
size_t ii = 0; ii < blockSize; ++ii )
246 inner[ii] =
static_cast<char>( key[ii] ^ 0x36 );
247 outer[ii] =
static_cast<char>( key[ii] ^ 0x5c );
250 std::array<uint8_t, 20> innerHash = sha1( inner + aData );
252 return sha1( outer + std::string( innerHash.begin(), innerHash.end() ) );
257 const wxString& aKeyType,
const std::string& aRawKey )
259 const int port = aPort > 0 ? aPort : 22;
260 const wxString plain = hostToken( aHost, 22 );
263 const wxString bracketed = wxString::Format( wxS(
"[%s]:%d" ), aHost.Lower(), port );
265 bool revoked =
false;
266 bool matched =
false;
267 bool mismatched =
false;
268 bool otherType =
false;
270 for( wxString line : wxSplit( aKnownHostsText,
'\n',
'\0' ) )
272 line.Trim(
true ).Trim(
false );
274 if( line.IsEmpty() || line.StartsWith( wxS(
"#" ) ) )
277 wxArrayString fields = wxStringTokenize( line, wxS(
" \t" ), wxTOKEN_STRTOK );
280 if( !fields.IsEmpty() && fields[0].StartsWith( wxS(
"@" ) ) )
283 fields.RemoveAt( 0 );
287 if( fields.size() < 3 || ( !marker.IsEmpty() && marker != wxS(
"@revoked" ) ) )
290 const bool sameKey = decodeBase64( fields[2] ) == aRawKey;
292 if( !marker.IsEmpty() )
294 if( sameKey && ( hostsFieldMatches( fields[0], bracketed ) || hostsFieldMatches( fields[0], plain ) ) )
300 if( !hostsFieldMatches( fields[0], bracketed ) && ( port != 22 || !hostsFieldMatches( fields[0], plain ) ) )
305 else if( fields[1] == aKeyType )
325 const wxString& aBase64Key )
327 return hostToken( aHost, aPort ) + wxS(
" " ) + aKeyType + wxS(
" " ) + aBase64Key;
333 if( aRawKey.size() < 4 )
334 return wxEmptyString;
336 const unsigned char* data =
reinterpret_cast<const unsigned char*
>( aRawKey.data() );
337 uint32_t nameLen = 0;
339 for(
size_t ii = 0; ii < 4; ++ii )
340 nameLen = ( nameLen << 8 ) | data[ii];
342 if( nameLen == 0 || nameLen > aRawKey.size() - 4 )
343 return wxEmptyString;
345 return wxString::FromUTF8( aRawKey.substr( 4, nameLen ) );
351 std::vector<unsigned char> digest( picosha2::k_digest_size );
352 picosha2::hash256( aRawKey.begin(), aRawKey.end(), digest.begin(), digest.end() );
354 wxString encoded = wxBase64Encode( digest.data(), digest.size() );
355 encoded.Replace( wxS(
"=" ), wxEmptyString );
365#if KIGIT_HAS_HOMEDIR_OPT
368 if( git_libgit2_opts( GIT_OPT_GET_HOMEDIR, &homeList ) == GIT_OK && homeList.ptr )
369 home = firstExistingDir( wxString::FromUTF8( homeList.ptr ) );
371 git_buf_dispose( &homeList );
377 git_libgit2_version( &major, &minor, &revision );
382 return wxEmptyString;
384 wxFileName
path( home, wxS(
"known_hosts" ) );
385 path.AppendDir( wxS(
".ssh" ) );
386 return path.GetFullPath();
395 return wxEmptyString;
397 std::ifstream in(
path.fn_str(), std::ios::binary );
400 return wxEmptyString;
402 std::string
text( ( std::istreambuf_iterator<char>( in ) ), std::istreambuf_iterator<char>() );
421 for( wxUniChar c : aKey.
keyType )
423 if( c <=
' ' || wxIscntrl( c ) )
427 wxFileName fn(
path );
433 if( !fn.DirExists() && !fn.Mkdir( 0700, wxPATH_MKDIR_FULL ) )
436 const wxString encoded = wxBase64Encode( aKey.
rawKey.data(), aKey.
rawKey.size() );
439 if( !endsWithNewline(
path ) )
440 line.insert( 0,
"\n" );
442 return appendToFile(
path, line );
448 if( aRetriedAfterTrust )
457 wxString host = aFailure.
host;
459 if( aFailure.
port != 22 )
460 host << wxS(
":" ) << aFailure.
port;
464 return wxString::Format(
_(
"The key for %s has been revoked. Ask your administrator before connecting "
471 return wxString::Format(
_(
"The key for %s has changed since the last connection. This can mean the server "
472 "was replaced or that someone is intercepting the connection. Ask your "
473 "administrator before connecting again." ),
477 return wxString::Format(
_(
"%s still could not be verified after its key was saved. Ask your administrator "
478 "before connecting again." ),
489 DWORD len = GetEnvironmentVariableW(
L"HOME",
nullptr, 0 );
490 std::vector<wchar_t> value( len + 1 );
492 if( len == 0 || GetEnvironmentVariableW(
L"HOME", value.data(), len ) == 0 )
493 return wxEmptyString;
495 return wxString( value.data() );
499 constexpr DWORD bufferSize = 4096 + 6;
502 for(
const wchar_t* tmpl : {
L"%HOME%\\",
L"%HOMEDRIVE%%HOMEPATH%\\",
L"%USERPROFILE%\\" } )
504 std::vector<wchar_t>
path( bufferSize );
505 DWORD len = ExpandEnvironmentStringsW( tmpl,
path.data(), bufferSize );
507 if( len == 0 || len > bufferSize ||
path[0] ==
L'%' || _waccess(
path.data(), 0 ) != 0 )
510 return wxString(
path.data() );
513 return wxEmptyString;
518 if( aRevision != 1 && ( std::getenv(
"APP_SANDBOX_CONTAINER_ID" ) || getuid() != geteuid() ) )
521 passwd* found =
nullptr;
522 std::vector<char> buffer( 1024 );
525 while( ( rc = getpwuid_r( geteuid(), &entry, buffer.data(), buffer.size(), &found ) ) == ERANGE
526 && buffer.size() < 65536 )
528 buffer.resize( buffer.size() * 2 );
531 if( rc == 0 && found && found->pw_dir )
532 home = wxString( found->pw_dir, wxConvLibc );
534 else if(
const char* value = std::getenv(
"HOME" ) )
536 home = wxString( value, wxConvLibc );
540 return aRevision == 1 ? home : firstExistingDir( home );
APIEXPORT bool AddKnownHost(const HOST_KEY_FAILURE &aKey)
Append the refused key to KnownHostsPath(), creating the file and its folder when missing.
KNOWN_HOST_STATE
How a server's host key stands against known_hosts.
@ OTHER_KEY_TYPE
Entries name the host only with keys of other types.
@ MISMATCH
Entries name the host with another key of this type, and none with this one.
@ UNKNOWN
No entry names the host with a key of this type.
@ MATCH
An entry names the host with this key.
@ REVOKED
A @revoked entry carries this key.
APIEXPORT wxString Libgit2LegacyHome(int aRevision)
The home folder libgit2 1.5 reads .ssh/known_hosts under, which it has no option to report.
APIEXPORT wxString FormatKnownHostsLine(const wxString &aHost, int aPort, const wxString &aKeyType, const wxString &aBase64Key)
The known_hosts line ssh would write for a key, without a line ending.
APIEXPORT KNOWN_HOST_STATE LookupKnownHost(const wxString &aKnownHostsText, const wxString &aHost, int aPort, const wxString &aKeyType, const std::string &aRawKey)
Classify a server's key against the text of a known_hosts file, as OpenSSH reads it.
APIEXPORT wxString ReadKnownHosts()
The text of KnownHostsPath(). Empty when it is missing or cannot be read.
APIEXPORT bool MayOfferTrust(const HOST_KEY_FAILURE &aFailure, bool aRetriedAfterTrust)
Whether the user may be asked to trust a refused key.
APIEXPORT wxString HostKeyType(const std::string &aRawKey)
The type a raw SSH key names for itself, such as ssh-ed25519. Empty when the key is malformed.
APIEXPORT wxString HostKeyFingerprint(const std::string &aRawKey)
SHA256 of a raw key in unpadded base64, as ssh shows it after "SHA256:".
APIEXPORT wxString HostKeyRefusalMessage(const HOST_KEY_FAILURE &aFailure)
Why a key the user may not trust was refused, and what to do next.
APIEXPORT std::array< uint8_t, 20 > HmacSha1(const std::string &aKey, const std::string &aData)
HMAC-SHA1 (RFC 2104), as OpenSSH hashes host names in known_hosts.
APIEXPORT wxString KnownHostsPath()
The known_hosts file libgit2 checks host keys against. Empty when it has no home folder.
A server key the connection refused, for the UI to offer to trust or to report.
wxString keyType
As known_hosts names it, such as ssh-ed25519.
std::string rawKey
The key as the server sent it.
static std::string decoded(const std::string &aBase64)
wxString result
Test unit parsing edge cases and error handling.