credentials_test.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  1. package git
  2. import (
  3. "os"
  4. "path/filepath"
  5. "strings"
  6. "testing"
  7. )
  8. func newTestCredentialStore(t *testing.T) (*CredentialStore, *fakeDatabase) {
  9. t.Helper()
  10. database := newFakeDatabase()
  11. store, err := newCredentialStore(database, filepath.Join(t.TempDir(), "keys"))
  12. if err != nil {
  13. t.Fatalf("newCredentialStore() returned error: %v", err)
  14. }
  15. return store, database
  16. }
  17. func TestCredentialSaveAndGet(t *testing.T) {
  18. store, _ := newTestCredentialStore(t)
  19. if err := store.Save("toby", "github.com", "tobychui", "ghp_secret_token"); err != nil {
  20. t.Fatalf("Save() returned error: %v", err)
  21. }
  22. credential, found := store.Get("toby", "github.com")
  23. if !found {
  24. t.Fatalf("Get() = not found, want the saved credential")
  25. }
  26. if credential.Username != "tobychui" {
  27. t.Errorf("Username = %q, want %q", credential.Username, "tobychui")
  28. }
  29. if credential.Token != "ghp_secret_token" {
  30. t.Errorf("Token = %q, want %q", credential.Token, "ghp_secret_token")
  31. }
  32. if credential.Host != "github.com" {
  33. t.Errorf("Host = %q, want %q", credential.Host, "github.com")
  34. }
  35. }
  36. func TestCredentialTokenIsEncryptedAtRest(t *testing.T) {
  37. store, database := newTestCredentialStore(t)
  38. if err := store.Save("toby", "github.com", "tobychui", "ghp_super_secret"); err != nil {
  39. t.Fatalf("Save() returned error: %v", err)
  40. }
  41. entries, err := database.ListTable(credentialTable)
  42. if err != nil {
  43. t.Fatalf("ListTable() returned error: %v", err)
  44. }
  45. if len(entries) != 1 {
  46. t.Fatalf("stored %d records, want 1", len(entries))
  47. }
  48. raw := string(entries[0][1])
  49. if strings.Contains(raw, "ghp_super_secret") {
  50. t.Errorf("stored record contains the plaintext token: %s", raw)
  51. }
  52. if !strings.Contains(raw, "tobychui") {
  53. t.Errorf("stored record should keep the user name readable for pre-fill: %s", raw)
  54. }
  55. }
  56. func TestCredentialTokenIsNeverMarshalled(t *testing.T) {
  57. store, _ := newTestCredentialStore(t)
  58. if err := store.Save("toby", "github.com", "tobychui", "ghp_secret"); err != nil {
  59. t.Fatalf("Save() returned error: %v", err)
  60. }
  61. //List is what the front-end receives; it must not carry any secret
  62. for _, credential := range store.List("toby") {
  63. if credential.Token != "" {
  64. t.Errorf("List() returned a populated Token field, want it empty")
  65. }
  66. }
  67. }
  68. func TestCredentialsAreIsolatedPerUser(t *testing.T) {
  69. store, _ := newTestCredentialStore(t)
  70. if err := store.Save("alice", "github.com", "alice-gh", "alice-token"); err != nil {
  71. t.Fatalf("Save() returned error: %v", err)
  72. }
  73. if err := store.Save("bob", "github.com", "bob-gh", "bob-token"); err != nil {
  74. t.Fatalf("Save() returned error: %v", err)
  75. }
  76. alice, found := store.Get("alice", "github.com")
  77. if !found || alice.Token != "alice-token" {
  78. t.Errorf("Get(alice) = %+v, want alice's own token", alice)
  79. }
  80. bob, found := store.Get("bob", "github.com")
  81. if !found || bob.Token != "bob-token" {
  82. t.Errorf("Get(bob) = %+v, want bob's own token", bob)
  83. }
  84. if list := store.List("alice"); len(list) != 1 || list[0].Username != "alice-gh" {
  85. t.Errorf("List(alice) = %+v, want only alice's entry", list)
  86. }
  87. }
  88. func TestCredentialRemove(t *testing.T) {
  89. store, _ := newTestCredentialStore(t)
  90. if err := store.Save("toby", "github.com", "tobychui", "token"); err != nil {
  91. t.Fatalf("Save() returned error: %v", err)
  92. }
  93. if err := store.Remove("toby", "github.com"); err != nil {
  94. t.Fatalf("Remove() returned error: %v", err)
  95. }
  96. if _, found := store.Get("toby", "github.com"); found {
  97. t.Errorf("Get() found a credential after Remove(), want it gone")
  98. }
  99. //Removing something that is not there is not an error
  100. if err := store.Remove("toby", "github.com"); err != nil {
  101. t.Errorf("Remove() on a missing credential = %v, want nil", err)
  102. }
  103. }
  104. func TestCredentialSaveValidation(t *testing.T) {
  105. store, _ := newTestCredentialStore(t)
  106. tests := []struct {
  107. name string
  108. owner string
  109. host string
  110. remoteUser string
  111. token string
  112. }{
  113. {name: "empty owner", owner: "", host: "github.com", remoteUser: "u", token: "t"},
  114. {name: "empty host", owner: "toby", host: "", remoteUser: "u", token: "t"},
  115. {name: "empty token", owner: "toby", host: "github.com", remoteUser: "u", token: ""},
  116. {name: "whitespace token", owner: "toby", host: "github.com", remoteUser: "u", token: " "},
  117. }
  118. for _, test := range tests {
  119. t.Run(test.name, func(t *testing.T) {
  120. if err := store.Save(test.owner, test.host, test.remoteUser, test.token); err == nil {
  121. t.Errorf("Save() with %s = nil error, want an error", test.name)
  122. }
  123. })
  124. }
  125. }
  126. func TestCredentialGetMissing(t *testing.T) {
  127. store, _ := newTestCredentialStore(t)
  128. tests := []struct {
  129. name string
  130. owner string
  131. host string
  132. }{
  133. {name: "nothing stored", owner: "toby", host: "github.com"},
  134. {name: "empty owner", owner: "", host: "github.com"},
  135. {name: "empty host", owner: "toby", host: ""},
  136. }
  137. for _, test := range tests {
  138. t.Run(test.name, func(t *testing.T) {
  139. if _, found := store.Get(test.owner, test.host); found {
  140. t.Errorf("Get(%q, %q) = found, want not found", test.owner, test.host)
  141. }
  142. })
  143. }
  144. }
  145. func TestCredentialResolveForRemote(t *testing.T) {
  146. store, _ := newTestCredentialStore(t)
  147. if err := store.Save("toby", "github.com", "tobychui", "token"); err != nil {
  148. t.Fatalf("Save() returned error: %v", err)
  149. }
  150. tests := []struct {
  151. name string
  152. remoteURL string
  153. wantFound bool
  154. }{
  155. {name: "https url", remoteURL: "https://github.com/tobychui/arozos.git", wantFound: true},
  156. {name: "https url with credentials", remoteURL: "https://user@github.com/a/b.git", wantFound: true},
  157. {name: "scp style", remoteURL: "git@github.com:tobychui/arozos.git", wantFound: true},
  158. {name: "different host", remoteURL: "https://gitlab.com/a/b.git", wantFound: false},
  159. }
  160. for _, test := range tests {
  161. t.Run(test.name, func(t *testing.T) {
  162. _, found := store.ResolveForRemote("toby", test.remoteURL)
  163. if found != test.wantFound {
  164. t.Errorf("ResolveForRemote(%q) found = %v, want %v", test.remoteURL, found, test.wantFound)
  165. }
  166. })
  167. }
  168. }
  169. func TestNormaliseHost(t *testing.T) {
  170. tests := []struct {
  171. name string
  172. input string
  173. want string
  174. }{
  175. {name: "bare host", input: "github.com", want: "github.com"},
  176. {name: "mixed case", input: "GitHub.COM", want: "github.com"},
  177. {name: "surrounding spaces", input: " github.com ", want: "github.com"},
  178. {name: "https url", input: "https://github.com/a/b.git", want: "github.com"},
  179. {name: "http url with port", input: "http://git.local:3000/a/b.git", want: "git.local"},
  180. {name: "scp syntax", input: "git@github.com:a/b.git", want: "github.com"},
  181. {name: "host with port", input: "git.local:3000", want: "git.local"},
  182. {name: "empty", input: "", want: ""},
  183. }
  184. for _, test := range tests {
  185. t.Run(test.name, func(t *testing.T) {
  186. if got := normaliseHost(test.input); got != test.want {
  187. t.Errorf("normaliseHost(%q) = %q, want %q", test.input, got, test.want)
  188. }
  189. })
  190. }
  191. }
  192. func TestEncryptDecryptRoundTrip(t *testing.T) {
  193. key := make([]byte, 32)
  194. for i := range key {
  195. key[i] = byte(i)
  196. }
  197. tests := []struct {
  198. name string
  199. plaintext string
  200. }{
  201. {name: "simple token", plaintext: "ghp_abcdefghijklmnop"},
  202. {name: "empty string", plaintext: ""},
  203. {name: "unicode", plaintext: "pässwörd-中文"},
  204. {name: "long secret", plaintext: strings.Repeat("s", 4096)},
  205. }
  206. for _, test := range tests {
  207. t.Run(test.name, func(t *testing.T) {
  208. encrypted, err := encryptSecret(key, test.plaintext)
  209. if err != nil {
  210. t.Fatalf("encryptSecret() returned error: %v", err)
  211. }
  212. if encrypted == test.plaintext && test.plaintext != "" {
  213. t.Fatalf("encryptSecret() returned the plaintext unchanged")
  214. }
  215. decrypted, err := decryptSecret(key, encrypted)
  216. if err != nil {
  217. t.Fatalf("decryptSecret() returned error: %v", err)
  218. }
  219. if decrypted != test.plaintext {
  220. t.Errorf("round trip = %q, want %q", decrypted, test.plaintext)
  221. }
  222. })
  223. }
  224. }
  225. func TestEncryptSecretUsesFreshNonce(t *testing.T) {
  226. key := make([]byte, 32)
  227. first, err := encryptSecret(key, "same input")
  228. if err != nil {
  229. t.Fatalf("encryptSecret() returned error: %v", err)
  230. }
  231. second, err := encryptSecret(key, "same input")
  232. if err != nil {
  233. t.Fatalf("encryptSecret() returned error: %v", err)
  234. }
  235. if first == second {
  236. t.Errorf("encrypting the same value twice produced identical ciphertext, want distinct nonces")
  237. }
  238. }
  239. func TestDecryptSecretRejectsTampering(t *testing.T) {
  240. key := make([]byte, 32)
  241. otherKey := make([]byte, 32)
  242. otherKey[0] = 0xFF
  243. encrypted, err := encryptSecret(key, "secret")
  244. if err != nil {
  245. t.Fatalf("encryptSecret() returned error: %v", err)
  246. }
  247. tests := []struct {
  248. name string
  249. key []byte
  250. input string
  251. }{
  252. {name: "wrong key", key: otherKey, input: encrypted},
  253. {name: "not base64", key: key, input: "!!!not base64!!!"},
  254. {name: "truncated", key: key, input: "AAAA"},
  255. {name: "empty", key: key, input: ""},
  256. }
  257. for _, test := range tests {
  258. t.Run(test.name, func(t *testing.T) {
  259. if _, err := decryptSecret(test.key, test.input); err == nil {
  260. t.Errorf("decryptSecret() with %s = nil error, want an error", test.name)
  261. }
  262. })
  263. }
  264. }
  265. func TestLoadOrCreateKeyIsStable(t *testing.T) {
  266. keyStore := filepath.Join(t.TempDir(), "keys")
  267. first, err := loadOrCreateKey(keyStore)
  268. if err != nil {
  269. t.Fatalf("loadOrCreateKey() returned error: %v", err)
  270. }
  271. if len(first) != 32 {
  272. t.Fatalf("key length = %d, want 32", len(first))
  273. }
  274. second, err := loadOrCreateKey(keyStore)
  275. if err != nil {
  276. t.Fatalf("loadOrCreateKey() second call returned error: %v", err)
  277. }
  278. if string(first) != string(second) {
  279. t.Errorf("loadOrCreateKey() returned a different key on the second call")
  280. }
  281. }
  282. func TestLoadOrCreateKeyRegeneratesDamagedFile(t *testing.T) {
  283. keyStore := filepath.Join(t.TempDir(), "keys")
  284. if _, err := loadOrCreateKey(keyStore); err != nil {
  285. t.Fatalf("loadOrCreateKey() returned error: %v", err)
  286. }
  287. if err := os.WriteFile(filepath.Join(keyStore, keyFileName), []byte("corrupted"), 0600); err != nil {
  288. t.Fatalf("cannot damage key file: %v", err)
  289. }
  290. key, err := loadOrCreateKey(keyStore)
  291. if err != nil {
  292. t.Fatalf("loadOrCreateKey() on a damaged file returned error: %v", err)
  293. }
  294. if len(key) != 32 {
  295. t.Errorf("regenerated key length = %d, want 32", len(key))
  296. }
  297. }
  298. func TestNewCredentialStoreValidation(t *testing.T) {
  299. tests := []struct {
  300. name string
  301. database CredentialDatabase
  302. keyStorePath string
  303. }{
  304. {name: "nil database", database: nil, keyStorePath: t.TempDir()},
  305. {name: "empty key path", database: newFakeDatabase(), keyStorePath: ""},
  306. }
  307. for _, test := range tests {
  308. t.Run(test.name, func(t *testing.T) {
  309. if _, err := newCredentialStore(test.database, test.keyStorePath); err == nil {
  310. t.Errorf("newCredentialStore() with %s = nil error, want an error", test.name)
  311. }
  312. })
  313. }
  314. }
  315. func TestManagerWithoutDatabaseHasNoCredentialStore(t *testing.T) {
  316. manager, err := NewManager(Options{})
  317. if err != nil {
  318. t.Fatalf("NewManager() returned error: %v", err)
  319. }
  320. if manager.Credentials() != nil {
  321. t.Errorf("Credentials() = non-nil without a database, want nil")
  322. }
  323. }