Skip to content

Commit 71e1b4b

Browse files
peffgitster
authored andcommitted
credentials: add "store" helper
This is like "cache", except that we actually put the credentials on disk. This can be terribly insecure, of course, but we do what we can to protect them by filesystem permissions, and we warn the user in the documentation. This is not unlike using .netrc to store entries, but it's a little more user-friendly. Instead of putting credentials in place ahead of time, we transparently store them after prompting the user for them once. Signed-off-by: Jeff King <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent c505116 commit 71e1b4b

File tree

6 files changed

+248
-0
lines changed

6 files changed

+248
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
/git-count-objects
3333
/git-credential-cache
3434
/git-credential-cache--daemon
35+
/git-credential-store
3536
/git-cvsexportcommit
3637
/git-cvsimport
3738
/git-cvsserver
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
git-credential-store(1)
2+
=======================
3+
4+
NAME
5+
----
6+
git-credential-store - helper to store credentials on disk
7+
8+
SYNOPSIS
9+
--------
10+
-------------------
11+
git config credential.helper 'store [options]'
12+
-------------------
13+
14+
DESCRIPTION
15+
-----------
16+
17+
NOTE: Using this helper will store your passwords unencrypted on disk,
18+
protected only by filesystem permissions. If this is not an acceptable
19+
security tradeoff, try linkgit:git-credential-cache[1], or find a helper
20+
that integrates with secure storage provided by your operating system.
21+
22+
This command stores credentials indefinitely on disk for use by future
23+
git programs.
24+
25+
You probably don't want to invoke this command directly; it is meant to
26+
be used as a credential helper by other parts of git. See
27+
linkgit:gitcredentials[7] or `EXAMPLES` below.
28+
29+
OPTIONS
30+
-------
31+
32+
--store=<path>::
33+
34+
Use `<path>` to store credentials. The file will have its
35+
filesystem permissions set to prevent other users on the system
36+
from reading it, but will not be encrypted or otherwise
37+
protected. Defaults to `~/.git-credentials`.
38+
39+
EXAMPLES
40+
--------
41+
42+
The point of this helper is to reduce the number of times you must type
43+
your username or password. For example:
44+
45+
------------------------------------------
46+
$ git config credential.helper store
47+
$ git push http://example.com/repo.git
48+
Username: <type your username>
49+
Password: <type your password>
50+
51+
[several days later]
52+
$ git push http://example.com/repo.git
53+
[your credentials are used automatically]
54+
------------------------------------------
55+
56+
STORAGE FORMAT
57+
--------------
58+
59+
The `.git-credentials` file is stored in plaintext. Each credential is
60+
stored on its own line as a URL like:
61+
62+
------------------------------
63+
https://user:[email protected]
64+
------------------------------
65+
66+
When git needs authentication for a particular URL context,
67+
credential-store will consider that context a pattern to match against
68+
each entry in the credentials file. If the protocol, hostname, and
69+
username (if we already have one) match, then the password is returned
70+
to git. See the discussion of configuration in linkgit:gitcredentials[7]
71+
for more information.
72+
73+
GIT
74+
---
75+
Part of the linkgit:git[1] suite

Documentation/gitcredentials.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ cache::
7171
Cache credentials in memory for a short period of time. See
7272
linkgit:git-credential-cache[1] for details.
7373

74+
store::
75+
76+
Store credentials indefinitely on disk. See
77+
linkgit:git-credential-store[1] for details.
78+
7479
You may also have third-party helpers installed; search for
7580
`credential-*` in the output of `git help -a`, and consult the
7681
documentation of individual helpers. Once you have selected a helper,

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,7 @@ PROGRAM_OBJS += show-index.o
429429
PROGRAM_OBJS += upload-pack.o
430430
PROGRAM_OBJS += http-backend.o
431431
PROGRAM_OBJS += sh-i18n--envsubst.o
432+
PROGRAM_OBJS += credential-store.o
432433

433434
PROGRAMS += $(patsubst %.o,git-%$X,$(PROGRAM_OBJS))
434435

credential-store.c

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
#include "cache.h"
2+
#include "credential.h"
3+
#include "string-list.h"
4+
#include "parse-options.h"
5+
6+
static struct lock_file credential_lock;
7+
8+
static void parse_credential_file(const char *fn,
9+
struct credential *c,
10+
void (*match_cb)(struct credential *),
11+
void (*other_cb)(struct strbuf *))
12+
{
13+
FILE *fh;
14+
struct strbuf line = STRBUF_INIT;
15+
struct credential entry = CREDENTIAL_INIT;
16+
17+
fh = fopen(fn, "r");
18+
if (!fh) {
19+
if (errno != ENOENT)
20+
die_errno("unable to open %s", fn);
21+
return;
22+
}
23+
24+
while (strbuf_getline(&line, fh, '\n') != EOF) {
25+
credential_from_url(&entry, line.buf);
26+
if (entry.username && entry.password &&
27+
credential_match(c, &entry)) {
28+
if (match_cb) {
29+
match_cb(&entry);
30+
break;
31+
}
32+
}
33+
else if (other_cb)
34+
other_cb(&line);
35+
}
36+
37+
credential_clear(&entry);
38+
strbuf_release(&line);
39+
fclose(fh);
40+
}
41+
42+
static void print_entry(struct credential *c)
43+
{
44+
printf("username=%s\n", c->username);
45+
printf("password=%s\n", c->password);
46+
}
47+
48+
static void print_line(struct strbuf *buf)
49+
{
50+
strbuf_addch(buf, '\n');
51+
write_or_die(credential_lock.fd, buf->buf, buf->len);
52+
}
53+
54+
static void rewrite_credential_file(const char *fn, struct credential *c,
55+
struct strbuf *extra)
56+
{
57+
if (hold_lock_file_for_update(&credential_lock, fn, 0) < 0)
58+
die_errno("unable to get credential storage lock");
59+
if (extra)
60+
print_line(extra);
61+
parse_credential_file(fn, c, NULL, print_line);
62+
if (commit_lock_file(&credential_lock) < 0)
63+
die_errno("unable to commit credential store");
64+
}
65+
66+
static void store_credential(const char *fn, struct credential *c)
67+
{
68+
struct strbuf buf = STRBUF_INIT;
69+
70+
/*
71+
* Sanity check that what we are storing is actually sensible.
72+
* In particular, we can't make a URL without a protocol field.
73+
* Without either a host or pathname (depending on the scheme),
74+
* we have no primary key. And without a username and password,
75+
* we are not actually storing a credential.
76+
*/
77+
if (!c->protocol || !(c->host || c->path) ||
78+
!c->username || !c->password)
79+
return;
80+
81+
strbuf_addf(&buf, "%s://", c->protocol);
82+
strbuf_addstr_urlencode(&buf, c->username, 1);
83+
strbuf_addch(&buf, ':');
84+
strbuf_addstr_urlencode(&buf, c->password, 1);
85+
strbuf_addch(&buf, '@');
86+
if (c->host)
87+
strbuf_addstr_urlencode(&buf, c->host, 1);
88+
if (c->path) {
89+
strbuf_addch(&buf, '/');
90+
strbuf_addstr_urlencode(&buf, c->path, 0);
91+
}
92+
93+
rewrite_credential_file(fn, c, &buf);
94+
strbuf_release(&buf);
95+
}
96+
97+
static void remove_credential(const char *fn, struct credential *c)
98+
{
99+
/*
100+
* Sanity check that we actually have something to match
101+
* against. The input we get is a restrictive pattern,
102+
* so technically a blank credential means "erase everything".
103+
* But it is too easy to accidentally send this, since it is equivalent
104+
* to empty input. So explicitly disallow it, and require that the
105+
* pattern have some actual content to match.
106+
*/
107+
if (c->protocol || c->host || c->path || c->username)
108+
rewrite_credential_file(fn, c, NULL);
109+
}
110+
111+
static int lookup_credential(const char *fn, struct credential *c)
112+
{
113+
parse_credential_file(fn, c, print_entry, NULL);
114+
return c->username && c->password;
115+
}
116+
117+
int main(int argc, const char **argv)
118+
{
119+
const char * const usage[] = {
120+
"git credential-store [options] <action>",
121+
NULL
122+
};
123+
const char *op;
124+
struct credential c = CREDENTIAL_INIT;
125+
char *file = NULL;
126+
struct option options[] = {
127+
OPT_STRING(0, "file", &file, "path",
128+
"fetch and store credentials in <path>"),
129+
OPT_END()
130+
};
131+
132+
umask(077);
133+
134+
argc = parse_options(argc, argv, NULL, options, usage, 0);
135+
if (argc != 1)
136+
usage_with_options(usage, options);
137+
op = argv[0];
138+
139+
if (!file)
140+
file = expand_user_path("~/.git-credentials");
141+
if (!file)
142+
die("unable to set up default path; use --file");
143+
144+
if (credential_read(&c, stdin) < 0)
145+
die("unable to read credential");
146+
147+
if (!strcmp(op, "get"))
148+
lookup_credential(file, &c);
149+
else if (!strcmp(op, "erase"))
150+
remove_credential(file, &c);
151+
else if (!strcmp(op, "store"))
152+
store_credential(file, &c);
153+
else
154+
; /* Ignore unknown operation. */
155+
156+
return 0;
157+
}

t/t0302-credential-store.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#!/bin/sh
2+
3+
test_description='credential-store tests'
4+
. ./test-lib.sh
5+
. "$TEST_DIRECTORY"/lib-credential.sh
6+
7+
helper_test store
8+
9+
test_done

0 commit comments

Comments
 (0)