1 /* Copyright 2021 Jaakko Keränen <jaakko.keranen@iki.fi>
2 
3 Redistribution and use in source and binary forms, with or without
4 modification, are permitted provided that the following conditions are met:
5 
6 1. Redistributions of source code must retain the above copyright notice, this
7    list of conditions and the following disclaimer.
8 2. Redistributions in binary form must reproduce the above copyright notice,
9    this list of conditions and the following disclaimer in the documentation
10    and/or other materials provided with the distribution.
11 
12 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19 ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22 
23 #include "certimportwidget.h"
24 
25 #include "app.h"
26 #include "color.h"
27 #include "command.h"
28 #include "gmcerts.h"
29 #include "inputwidget.h"
30 #include "labelwidget.h"
31 #include "text.h"
32 #include "ui/util.h"
33 
34 #if defined (iPlatformAppleMobile)
35 #   include "ios.h"
36 #endif
37 
38 #include <the_Foundation/file.h>
39 #include <the_Foundation/tlsrequest.h>
40 #include <the_Foundation/path.h>
41 #include <SDL_clipboard.h>
42 
43 iDefineObjectConstruction(CertImportWidget)
44 
45 struct Impl_CertImportWidget {
46     iWidget widget;
47     iLabelWidget *info;
48     iLabelWidget *crtLabel;
49     iLabelWidget *keyLabel;
50     iInputWidget *notes;
51     iTlsCertificate *cert;
52 };
53 
54 static const char *infoText_ = "${dlg.certimport.help}";
55 
tryImport_CertImportWidget_(iCertImportWidget * d,const iBlock * data)56 static iBool tryImport_CertImportWidget_(iCertImportWidget *d, const iBlock *data) {
57     iBool ok = iFalse;
58     iString pem;
59     initBlock_String(&pem, data);
60     iTlsCertificate *newCert = newPemKey_TlsCertificate(&pem, &pem);
61     const iBool gotNewCrt = !isEmpty_TlsCertificate(newCert);
62     const iBool gotNewKey = hasPrivateKey_TlsCertificate(newCert);
63     if (d->cert && (gotNewCrt ^ gotNewKey)) { /* One new part? Merge with existing. */
64         const iString *crt = collect_String(pem_TlsCertificate(gotNewCrt ? newCert : d->cert));
65         const iString *key = collect_String(privateKeyPem_TlsCertificate(gotNewKey ? newCert : d->cert));
66         delete_TlsCertificate(d->cert);
67         delete_TlsCertificate(newCert);
68         d->cert = newPemKey_TlsCertificate(crt, key);
69         ok = iTrue;
70     }
71     else if (gotNewCrt || gotNewKey) {
72         delete_TlsCertificate(d->cert);
73         d->cert = newCert;
74         ok = iTrue;
75     }
76     else {
77         delete_TlsCertificate(newCert);
78     }
79     deinit_String(&pem);
80     /* Update the labels. */ {
81         if (d->cert && !isEmpty_TlsCertificate(d->cert)) {
82             updateTextCStr_LabelWidget(
83                 d->crtLabel,
84                 format_CStr("%s%s",
85                             uiTextAction_ColorEscape,
86                             cstrCollect_String(subject_TlsCertificate(d->cert))));
87             setFrameColor_Widget(as_Widget(d->crtLabel), uiTextAction_ColorId);
88         }
89         else {
90             updateTextCStr_LabelWidget(d->crtLabel, uiTextCaution_ColorEscape "${dlg.certimport.nocert}");
91             setFrameColor_Widget(as_Widget(d->crtLabel), uiTextCaution_ColorId);
92         }
93         if (d->cert && hasPrivateKey_TlsCertificate(d->cert)) {
94             iString *fng = collect_String(
95                 hexEncode_Block(collect_Block(privateKeyFingerprint_TlsCertificate(d->cert))));
96             insertData_Block(&fng->chars, size_String(fng) / 2, "\n", 1);
97             updateTextCStr_LabelWidget(
98                 d->keyLabel, format_CStr("%s%s", uiTextAction_ColorEscape, cstr_String(fng)));
99             setFrameColor_Widget(as_Widget(d->keyLabel), uiTextAction_ColorId);
100         }
101         else {
102             updateTextCStr_LabelWidget(d->keyLabel, uiTextCaution_ColorEscape "${dlg.certimport.nokey}");
103             setFrameColor_Widget(as_Widget(d->keyLabel), uiTextCaution_ColorId);
104         }
105     }
106     return ok;
107 }
108 
init_CertImportWidget(iCertImportWidget * d)109 void init_CertImportWidget(iCertImportWidget *d) {
110     iWidget *w = as_Widget(d);
111     const iMenuItem actions[] = {
112 #if defined (iPlatformAppleMobile)
113         { "${dlg.certimport.pickfile}", 0, 0, "certimport.pickfile" },
114         { "---" },
115 #endif
116         { "${cancel}" },
117         { uiTextAction_ColorEscape "${dlg.certimport.import}",
118           SDLK_RETURN, KMOD_PRIMARY,
119           "certimport.accept" }
120     };
121     init_Widget(w);
122     setId_Widget(w, "certimport");
123     d->cert = NULL;
124     if (isUsingPanelLayout_Mobile()) {
125         initPanels_Mobile(w, NULL, (iMenuItem[]){
126             { "title id:heading.certimport" },
127             { format_CStr("label id:certimport.info text:%s", infoText_) },
128             //{ "padding" },
129             { "label id:certimport.crt nowrap:1 frame:1" },
130             { "padding arg:0.25" },
131             { "label id:certimport.key nowrap:1 frame:1" },
132             { "heading text:${dlg.certimport.notes}" },
133             { "input id:certimport.notes hint:hint.certimport.description noheading:1" },
134             { NULL }
135         }, actions, iElemCount(actions));
136         d->info     = findChild_Widget(w, "certimport.info");
137         d->crtLabel = findChild_Widget(w, "certimport.crt");
138         d->keyLabel = findChild_Widget(w, "certimport.key");
139         d->notes    = findChild_Widget(w, "certimport.notes");
140         setFont_LabelWidget(d->crtLabel, uiContent_FontId);
141         setFont_LabelWidget(d->keyLabel, uiContent_FontId);
142         setFixedSize_Widget(as_Widget(d->crtLabel), init_I2(-1, gap_UI * 12));
143         setFixedSize_Widget(as_Widget(d->keyLabel), init_I2(-1, gap_UI * 12));
144     }
145     else {
146         /* This should behave similar to sheets. */
147         useSheetStyle_Widget(w);
148         addChildFlags_Widget(
149             w,
150             iClob(new_LabelWidget(uiHeading_ColorEscape "${heading.certimport}", NULL)),
151             frameless_WidgetFlag);
152         d->info = addChildFlags_Widget(w, iClob(new_LabelWidget(infoText_, NULL)), frameless_WidgetFlag);
153         addChild_Widget(w, iClob(makePadding_Widget(gap_UI)));
154         d->crtLabel = new_LabelWidget("", NULL); {
155             setFont_LabelWidget(d->crtLabel, uiContent_FontId);
156             addChildFlags_Widget(w, iClob(d->crtLabel), 0);
157         }
158         d->keyLabel = new_LabelWidget("", NULL); {
159             setFont_LabelWidget(d->keyLabel, uiContent_FontId);
160             addChild_Widget(w, iClob(makePadding_Widget(gap_UI)));
161             addChildFlags_Widget(w, iClob(d->keyLabel), 0);
162         }
163         addChild_Widget(w, iClob(makePadding_Widget(gap_UI)));
164         /* TODO: Use makeTwoColumnWidget_() */
165         iWidget *page = new_Widget(); {
166             setFlags_Widget(page, arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag, iTrue);
167             iWidget *headings = addChildFlags_Widget(
168                 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag);
169             iWidget *values = addChildFlags_Widget(
170                 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag);
171             addTwoColumnDialogInputField_Widget(
172                 headings,
173                 values,
174                 "${dlg.certimport.notes}",
175                 "",
176                 iClob(d->notes = newHint_InputWidget(0, "${hint.certimport.description}")));
177             as_Widget(d->notes)->rect.size.x = gap_UI * 70;
178         }
179         addChild_Widget(w, iClob(page));
180         arrange_Widget(w);
181         setFixedSize_Widget(as_Widget(d->crtLabel), init_I2(width_Widget(w) - 6.5 * gap_UI, gap_UI * 12));
182         setFixedSize_Widget(as_Widget(d->keyLabel), init_I2(width_Widget(w) - 6.5 * gap_UI, gap_UI * 12));
183         /* Buttons. */
184         addChild_Widget(w, iClob(makePadding_Widget(gap_UI)));
185         iWidget *buttons = makeDialogButtons_Widget(actions, iElemCount(actions));
186         addChild_Widget(w, iClob(buttons));
187     }
188     setFrameColor_Widget(as_Widget(d->crtLabel), uiTextCaution_ColorId);
189     setFrameColor_Widget(as_Widget(d->keyLabel), uiTextCaution_ColorId);
190     if (deviceType_App() != desktop_AppDeviceType) {
191         /* Try auto-pasting. */
192         postCommand_App("certimport.paste");
193     }
194 }
195 
deinit_CertImportWidget(iCertImportWidget * d)196 void deinit_CertImportWidget(iCertImportWidget *d) {
197     delete_TlsCertificate(d->cert);
198 }
199 
isComplete_CertImportWidget_(const iCertImportWidget * d)200 static iBool isComplete_CertImportWidget_(const iCertImportWidget *d) {
201     return d->cert && !isEmpty_TlsCertificate(d->cert) && hasPrivateKey_TlsCertificate(d->cert);
202 }
203 
setPageContent_CertImportWidget(iCertImportWidget * d,const iBlock * content)204 void setPageContent_CertImportWidget(iCertImportWidget *d, const iBlock *content) {
205     if (tryImport_CertImportWidget_(d, content)) {
206         setTextCStr_LabelWidget(d->info, infoText_);
207         if (isComplete_CertImportWidget_(d)) {
208             setFocus_Widget(as_Widget(d->notes));
209         }
210     }
211     else {
212         setTextCStr_LabelWidget(
213             d->info, format_CStr("${dlg.certimport.notfound.page}\n%s", infoText_));
214     }
215     arrange_Widget(as_Widget(d));
216 }
217 
tryImportFromClipboard_CertImportWidget_(iCertImportWidget * d)218 static iBool tryImportFromClipboard_CertImportWidget_(iCertImportWidget *d) {
219     return tryImport_CertImportWidget_(d, collect_Block(newCStr_Block(SDL_GetClipboardText())));
220 }
221 
tryImportFromFile_CertImportWidget_(iCertImportWidget * d,const iString * path)222 static iBool tryImportFromFile_CertImportWidget_(iCertImportWidget *d, const iString *path) {
223     iBool success = iFalse;
224     iFile *f = new_File(path);
225     if (open_File(f, readOnly_FileMode | text_FileMode)) {
226         if (tryImport_CertImportWidget_(d, collect_Block(readAll_File(f)))) {
227             success = iTrue;
228             if (isComplete_CertImportWidget_(d)) {
229                 setFocus_Widget(as_Widget(d->notes));
230             }
231         }
232         else {
233             makeSimpleMessage_Widget(uiTextCaution_ColorEscape "${heading.certimport.dropped}",
234                                      "${dlg.certimport.notfound}");
235         }
236     }
237     iRelease(f);
238     return success;
239 }
240 
processEvent_CertImportWidget_(iCertImportWidget * d,const SDL_Event * ev)241 static iBool processEvent_CertImportWidget_(iCertImportWidget *d, const SDL_Event *ev) {
242     iWidget *w = as_Widget(d);
243     if (ev->type == SDL_KEYDOWN) {
244         const int key  = ev->key.keysym.sym;
245         const int mods = keyMods_Sym(ev->key.keysym.mod);
246         if (key == SDLK_v && mods == KMOD_PRIMARY) {
247             if (!tryImportFromClipboard_CertImportWidget_(d)) {
248                 makeSimpleMessage_Widget(uiTextCaution_ColorEscape "${heading.certimport.pasted}",
249                                          "${dlg.certimport.notfound}");
250             }
251             postRefresh_App();
252             return iTrue;
253         }
254     }
255     if (isCommand_UserEvent(ev, "input.paste")) {
256         if (!tryImportFromClipboard_CertImportWidget_(d)) {
257             makeSimpleMessage_Widget(uiTextCaution_ColorEscape "${heading.certimport.pasted}",
258                                      "${dlg.certimport.notfound}");
259         }
260         postRefresh_App();
261         return iTrue;
262     }
263     if (isCommand_UserEvent(ev, "certimport.paste")) {
264         tryImportFromClipboard_CertImportWidget_(d);
265         return iTrue;
266     }
267     if (isCommand_Widget(w, ev, "cancel")) {
268         setupSheetTransition_Mobile(w, iFalse);
269         destroy_Widget(w);
270         return iTrue;
271     }
272     if (isCommand_Widget(w, ev, "certimport.accept")) {
273         if (d->cert && !isEmpty_TlsCertificate(d->cert) && hasPrivateKey_TlsCertificate(d->cert)) {
274             importIdentity_GmCerts(certs_App(), d->cert, text_InputWidget(d->notes));
275             d->cert = NULL; /* taken */
276             setupSheetTransition_Mobile(w, iFalse);
277             destroy_Widget(w);
278             postCommand_App("idents.changed");
279         }
280         return iTrue;
281     }
282 #if defined (iPlatformAppleMobile)
283     if (isCommand_UserEvent(ev, "certimport.pickfile")) {
284         const char *cmd = command_UserEvent(ev);
285         if (hasLabel_Command(cmd, "path")) {
286             const iString *path = collect_String(suffix_Command(cmd, "path"));
287             tryImportFromFile_CertImportWidget_(d, path);
288             remove(cstr_String(path)); /* it is a temporary copy */
289         }
290         else {
291             pickFile_iOS("certimport.pickfile");
292         }
293         return iTrue;
294     }
295 #endif
296     if (ev->type == SDL_DROPFILE) {
297         tryImportFromFile_CertImportWidget_(d, collectNewCStr_String(ev->drop.file));
298         return iTrue;
299     }
300     return processEvent_Widget(w, ev);
301 }
302 
draw_CertImportWidget_(const iCertImportWidget * d)303 static void draw_CertImportWidget_(const iCertImportWidget *d) {
304     draw_Widget(constAs_Widget(d));
305 }
306 
307 iBeginDefineSubclass(CertImportWidget, Widget)
308     .processEvent = (iAny *) processEvent_CertImportWidget_,
309     .draw         = (iAny *) draw_CertImportWidget_,
310 iEndDefineSubclass(CertImportWidget)
311