Line data Source code
1 : // SPDX-License-Identifier: LGPL-2.1-or-later
2 19 : import cockpit from "cockpit";
3 :
4 : import lister from "./ssh-list-public-keys.sh";
5 : import adder from "./ssh-add-public-key.sh";
6 :
7 19 : const _ = cockpit.gettext;
8 :
9 11 : function AuthorizedKeys (user_name, home_dir) {
10 11 : const self = this;
11 11 : const dir = home_dir + "/.ssh";
12 11 : const filename = dir + "/authorized_keys";
13 11 : let file = null;
14 11 : let watch = null;
15 11 : let last_tag = null;
16 :
17 11 : cockpit.event_target(self);
18 :
19 11 : self.keys = [];
20 11 : self.state = "loading";
21 :
22 0 : function process_failure (ex) {
23 0 : self.keys = [];
24 0 : if (ex.problem == "access-denied") {
25 0 : self.state = ex.problem;
26 0 : } else if (ex.problem == "not-found") {
27 0 : self.state = "ready";
28 0 : } else {
29 0 : self.state = "failed";
30 0 : console.warn("Error processing authentication keys: " + ex);
31 0 : }
32 0 : self.dispatchEvent("changed");
33 0 : }
34 :
35 11 : function update_keys(keys, tag) {
36 11 : if (tag !== last_tag)
37 11 : return;
38 :
39 11 : self.keys = keys;
40 11 : self.state = "ready";
41 11 : self.dispatchEvent("changed");
42 11 : }
43 :
44 : /*
45 : * Splits up a strings like:
46 : *
47 : * 2048 SHA256:AAAAB3NzaC1yc2EAAAADAQ Comment Here (RSA)
48 : * 2048 SHA256:AAAAB3NzaC1yc2EAAAADAQ (RSA)
49 : */
50 11 : const PUBKEY_RE = /^(\S+)\s+(\S+)\s+(.*)\((\S+)\)$/;
51 :
52 6 : function parse_pubkeys(input) {
53 6 : const keys = [];
54 :
55 6 : return cockpit.script(lister)
56 6 : .input(input + "\n")
57 6 : .then(output => {
58 6 : const lines = output.split("\n");
59 :
60 6 : for (let i = 0; i + 1 < lines.length; i += 2) {
61 6 : const obj = { raw: lines[i + 1] };
62 6 : keys.push(obj);
63 6 : const match = lines[i].trim().match(PUBKEY_RE);
64 6 : obj.valid = !!match && !!obj.raw;
65 6 : if (match) {
66 6 : obj.size = match[1];
67 6 : obj.fp = match[2];
68 6 : obj.comment = match[3].trim();
69 6 : if (obj.comment == "authorized_keys" || obj.comment == "no comment")
70 1 : obj.comment = null;
71 6 : obj.algorithm = match[4];
72 :
73 : /* Old ssh-keygen versions need us to find the comment ourselves */
74 1 : if (!obj.comment && obj.raw)
75 1 : obj.comment = obj.raw
76 1 : .split(" ")
77 1 : .splice(0, 2)
78 1 : .join(" ") || null;
79 6 : }
80 6 : }
81 6 : return keys;
82 6 : })
83 0 : .catch(ex => { // not-covered: OS error
84 0 : console.warn("Failed to list public keys:", ex.toString()); // not-covered: OS error
85 0 : return []; // not-covered: OS error
86 0 : });
87 6 : }
88 :
89 11 : function parse_keys(content, tag, ex) {
90 11 : last_tag = tag;
91 :
92 11 : if (ex)
93 2 : return process_failure(ex);
94 :
95 11 : if (!content)
96 8 : return update_keys([], tag);
97 :
98 6 : parse_pubkeys(content)
99 6 : .then(keys => update_keys(keys, tag));
100 11 : }
101 :
102 0 : self.add_key = function(key) {
103 0 : return parse_pubkeys(key)
104 0 : .then(keys => {
105 0 : const obj = keys[0];
106 0 : if (obj?.valid) {
107 0 : return cockpit
108 0 : .script(adder, [user_name, home_dir], { superuser: "try", err: "message" })
109 0 : .input(obj.raw + "\n")
110 : // eslint-disable-next-line prefer-promise-reject-errors
111 0 : .catch(ex => Promise.reject(_("Error saving authorized keys: ") + ex)); // not-covered: OS error
112 0 : } else {
113 0 : return Promise.reject(_("The key you provided was not valid."));
114 0 : }
115 0 : });
116 0 : };
117 :
118 1 : self.remove_key = function(key) {
119 1 : return file.modify(function(content) {
120 1 : let lines = null;
121 1 : const new_lines = [];
122 :
123 1 : if (!content)
124 0 : return null;
125 :
126 1 : lines = content.trim().split('\n');
127 1 : for (let i = 0; i < lines.length; i++) {
128 1 : if (lines[i] === key)
129 0 : key = undefined;
130 : else
131 0 : new_lines.push(lines[i]);
132 1 : }
133 1 : return new_lines.join("\n") || null;
134 1 : });
135 1 : };
136 :
137 4 : self.close = function() {
138 4 : if (watch)
139 4 : watch.remove();
140 :
141 4 : if (file)
142 4 : file.close();
143 4 : };
144 :
145 11 : file = cockpit.file(filename, { superuser: 'try' });
146 11 : watch = file.watch(parse_keys);
147 11 : }
148 :
149 11 : export function instance(user_name, home_dir) {
150 11 : return new AuthorizedKeys(user_name, home_dir);
151 11 : }
|