1 /*
2  * Copyright (C) 2018 Rafael Ostertag
3  *
4  * This file is part of YAPET.
5  *
6  * YAPET is free software: you can redistribute it and/or modify it under the
7  * terms of the GNU General Public License as published by the Free Software
8  * Foundation, either version 3 of the License, or (at your option) any later
9  * version.
10  *
11  * YAPET is distributed in the hope that it will be useful, but WITHOUT ANY
12  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13  * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
14  * details.
15  *
16  * You should have received a copy of the GNU General Public License along with
17  * YAPET.  If not, see <http://www.gnu.org/licenses/>.
18  *
19  * Additional permission under GNU GPL version 3 section 7
20  *
21  * If you modify this program, or any covered work, by linking or combining it
22  * with the OpenSSL project's OpenSSL library (or a modified version of that
23  * library), containing parts covered by the terms of the OpenSSL or SSLeay
24  * licenses, Rafael Ostertag grants you additional permission to convey the
25  * resulting work.  Corresponding Source for a non-source form of such a
26  * combination shall include the source code for the parts of OpenSSL used as
27  * well as that of the covered work.
28  */
29 
30 #ifdef HAVE_CONFIG_H
31 #include "config.h"
32 #endif
33 
34 #include <unistd.h>
35 
36 #include <cstdio>
37 #include <cstring>
38 #include <fstream>
39 #include <iostream>
40 
41 #include "aes256factory.hh"
42 #include "consts.h"
43 #include "csvimport.h"
44 #include "csvline.hh"
45 #include "file.h"
46 #include "intl.h"
47 
48 constexpr char NEW_LINE_CHARACTER{'\n'};
49 constexpr char DOUBLE_QUOTE{'"'};
50 
51 constexpr auto NUMBER_OF_FIELDS{5};
52 // the max line length. Computed from the field sizes of a YAPET password
53 // record.
54 constexpr int MAX_LINE_LENGTH{
55     yapet::PasswordRecord::NAME_SIZE + yapet::PasswordRecord::HOST_SIZE +
56     yapet::PasswordRecord::USERNAME_SIZE +
57     yapet::PasswordRecord::PASSWORD_SIZE + yapet::PasswordRecord::COMMENT_SIZE +
58     // for the separators
59     NUMBER_OF_FIELDS -
60     1
61     // null terminators, one for each field
62     - 5};
63 
logError(unsigned long lno,const std::string & errmsg)64 void CSVImport::logError(unsigned long lno, const std::string& errmsg) {
65     if (verbose) {
66         std::cout << 'e';
67         std::cout.flush();
68     }
69 
70     LogEntry tmp;
71     tmp.lineNumber = lno;
72     tmp.message = errmsg;
73 
74     logs.push_back(tmp);
75     had_errors = true;
76     num_errors++;
77 }
78 
csvLineToPasswordRecord(yapet::CSVLine & csvLine,std::unique_ptr<yapet::Crypto> & crypto)79 yapet::PasswordListItem CSVImport::csvLineToPasswordRecord(
80     yapet::CSVLine& csvLine, std::unique_ptr<yapet::Crypto>& crypto) {
81     yapet::PasswordRecord passwordRecord;
82 
83     passwordRecord.name(csvLine[0].c_str());
84     passwordRecord.host(csvLine[1].c_str());
85     passwordRecord.username(csvLine[2].c_str());
86     passwordRecord.password(csvLine[3].c_str());
87     passwordRecord.comment(csvLine[4].c_str());
88 
89     auto serializedRecord{passwordRecord.serialize()};
90     auto encryptedRecord{crypto->encrypt(serializedRecord)};
91 
92     return yapet::PasswordListItem{csvLine[0].c_str(), encryptedRecord};
93 }
94 
95 /**
96  * The constructor tests whether the given source file exists and can be
97  * read. May return a \c std::runtime_error if this is not the case
98  *
99  * @param src the file path of the source file.
100  *
101  * @param dst the file path of the destination file.
102  *
103  * @param sep the separator used for fields.
104  *
105  * @param verb enable/disable verbosity. Default \c true.
106  */
107 
CSVImport(std::string src,std::string dst,char sep,bool verb)108 CSVImport::CSVImport(std::string src, std::string dst, char sep, bool verb)
109     : srcfile(src),
110       dstfile(dst),
111       separator(sep),
112       verbose(verb),
113       had_errors(false),
114       num_errors(0) {
115     if (access(srcfile.c_str(), R_OK | F_OK) == -1) {
116         char msg[YAPET::Consts::EXCEPTION_MESSAGE_BUFFER_SIZE];
117         std::snprintf(msg, YAPET::Consts::EXCEPTION_MESSAGE_BUFFER_SIZE,
118                       _("Cannot access '%s'"), srcfile.c_str());
119         throw std::runtime_error(msg);
120     }
121 }
122 
123 namespace {
openCsvFile(const std::string & fileName)124 inline std::ifstream openCsvFile(const std::string& fileName) {
125     std::ifstream file{fileName};
126 
127     if (!file) {
128         char msg[YAPET::Consts::EXCEPTION_MESSAGE_BUFFER_SIZE];
129         std::snprintf(msg, YAPET::Consts::EXCEPTION_MESSAGE_BUFFER_SIZE,
130                       _("Cannot open '%s'"), fileName.c_str());
131         throw std::runtime_error(msg);
132     }
133 
134     return file;
135 }
136 
read(std::ifstream & csvFile,char & lookAhead)137 inline char read(std::ifstream& csvFile, char& lookAhead) {
138     char tmp = lookAhead;
139     csvFile.get(lookAhead);
140     return tmp;
141 }
142 
143 /**
144  * Read a line and honor escaped newline character.
145  */
readLine(std::ifstream & csvFile,char separator,CSVImport::line_number_type & lineNumber)146 inline std::string readLine(std::ifstream& csvFile, char separator,
147                             CSVImport::line_number_type& lineNumber) {
148     std::string line;
149 
150     bool inEscapedField = false;
151     bool startOfLine = true;
152 
153     char currentCharacter;
154     char lookAhead;
155 
156     csvFile.get(lookAhead);
157     while (!csvFile.eof()) {
158         currentCharacter = read(csvFile, lookAhead);
159 
160         if (currentCharacter == NEW_LINE_CHARACTER) {
161             lineNumber++;
162         }
163 
164         if (currentCharacter == NEW_LINE_CHARACTER && !inEscapedField) {
165             csvFile.putback(lookAhead);
166             csvFile.clear();
167             break;
168         }
169 
170         if (startOfLine && currentCharacter == DOUBLE_QUOTE) {
171             startOfLine = false;
172             inEscapedField = true;
173             line += DOUBLE_QUOTE;
174             continue;
175         }
176 
177         if (currentCharacter == DOUBLE_QUOTE && lookAhead == DOUBLE_QUOTE &&
178             inEscapedField) {
179             line += currentCharacter;
180             line += read(csvFile, lookAhead);
181             continue;
182         }
183 
184         if (currentCharacter == DOUBLE_QUOTE &&
185             (lookAhead == separator || lookAhead == NEW_LINE_CHARACTER)) {
186             inEscapedField = false;
187             line += currentCharacter;
188             continue;
189         }
190 
191         if (currentCharacter == separator && !inEscapedField) {
192             line += currentCharacter;
193 
194             if (lookAhead == DOUBLE_QUOTE) {
195                 inEscapedField = true;
196                 line += read(csvFile, lookAhead);
197             }
198             continue;
199         }
200 
201         line += currentCharacter;
202         startOfLine = false;
203     }
204 
205     if (inEscapedField) {
206         throw std::invalid_argument(_("'\"' mismatch"));
207     }
208 
209     return line;
210 }
211 
212 }  // namespace
213 
214 /**
215  * Does the import.
216  *
217  * @param pw the password set on the destination file.
218  */
import(const char * pw)219 void CSVImport::import(const char* pw) {
220     auto csvFile{::openCsvFile(srcfile)};
221 
222     auto password{yapet::toSecureArray(pw)};
223     auto keyingParameters{yapet::Key256::newDefaultKeyingParameters()};
224     std::shared_ptr<yapet::AbstractCryptoFactory> cryptoFactory{
225         new yapet::Aes256Factory{password, keyingParameters}};
226     auto crypto{cryptoFactory->crypto()};
227     std::unique_ptr<YAPET::File> yapetFile{
228         new YAPET::File{cryptoFactory, dstfile, true}};
229 
230     std::list<yapet::PasswordListItem> list;
231 
232     line_number_type lineNumber = 0;
233 
234     yapet::CSVLine csvLine{NUMBER_OF_FIELDS, separator};
235 
236     for (std::string line;
237          line = ::readLine(csvFile, separator, lineNumber), !csvFile.eof();) {
238         if (line.size() > MAX_LINE_LENGTH) {
239             logError(lineNumber, _("Line too long"));
240             continue;
241         }
242 
243         try {
244             csvLine.parseLine(line);
245         } catch (std::exception& e) {
246             logError(lineNumber, e.what());
247             continue;
248         }
249 
250         auto passwordListItem{csvLineToPasswordRecord(csvLine, crypto)};
251         list.push_back(passwordListItem);
252 
253         if (verbose) {
254             std::cout << ".";
255             std::cout.flush();
256         }
257     }
258 
259     if (verbose) std::cout << std::endl;
260 
261     yapetFile->save(list);
262     csvFile.close();
263 }
264 
265 /**
266  * Prints the log entries to stdout.
267  */
printLog() const268 void CSVImport::printLog() const {
269     if (logs.size() == 0) return;
270 
271     std::list<LogEntry>::const_iterator it = logs.begin();
272 
273     while (it != logs.end()) {
274         std::cout << _("Line ") << (*it).lineNumber << ": " << (*it).message
275                   << std::endl;
276         it++;
277     }
278 }
279