В наше время иррационализма, утечек данных с серверов, постоянного внедрения рекламы тут и там, мошенничества, неумелого программирования, приложения начинают не помогать, а раздражать, а то и вовсе могут нанести вред. Поэтому в купе с несправедливостью и желанием сделать всё самому, в этой статье, мне охота показать, как легко сделать одно из бытовых приложений своими руками, на примере приложения хранения паролей, с помощью текстового файла, шифрования и облака.

Как всё будет работать

Уверен, многие люди хранят все свои пароли в обычном текстом документе, в закодированном архиве и по возможности в облаке. Эта идея и легка в основу создания приложения, а точнее в необходимости автоматизировать этот процесс. Для хранения паролей будет использоваться файл формата xml, который будет шифроваться методом AES-256 и храниться в облаке. В качестве облака был выбран сервис «Яндекс диск».

Основной процесс – это загрузка файла с паролями из облака, затем загрузка файла в оперативную память, его расшифровка и вывод паролей в интерфейс пользователю. По завершению работы с паролями, в оперативной памяти формируется XML-структура с паролями, которая шифруется и сохраняется в файл. Затем этот файл загружается в облако.

Для показа работоспособности системы будут сделаны два клиентских приложения, которые по API-диска будут загружать и выгружать файл: приложение на компьютер и на телефон. Приложение на компьютер будет написано на языке программирования C++. Компилятор g++. Операционная система Windows 10. Интерфейс приложения разработан с помощью библиотеки wxWidgets, шифрование осуществляется с помощью библиотеки OpenSSL.  Работа с облаком осуществляется через java API-диска, с помощью JDK. Приложение на телефон написано на языке программирования Kotlin. Операционная система Android. Интерфейс и шифрование из java коробки. Работа с облаком по java API-диска.

Архитектура

Хранение паролей осуществляется в XML-формате. Основной принцип структуры – это разделить пароли на блоки. Каждый блок – это идентификатор либо почты, либо номера мобильного телефона. У каждого блока есть атрибуты “title”, “login”, “password”. В каждом блоке находятся элементы, связанные именно с этим блоком. В данном случае это сайты, зарегистрированные на почту или номер мобильного телефона. Так же у каждого элемента есть атрибуты “title”, “login”, “password”.

key.xml

<?xml version="1.0" encoding="UTF-8"?>
<root>
  <block title="www.yandex.ru" login="social" password="G!2c#yF7">
    <item title="www.facebook.com" login="john.doe" password="i1vr9bfbj"/>
    <item title="www.instagram.com" login="alice_wonde" password="Rq1z$Tm4"/>
    <item title="www.vk.ru" login="janesmith" password="b1Pz@kN5"/>
  </block>
  <block title="www.google.com" login="pay" password="y4Gv@nH8">
    <item title="www.amazon.com" login="mikebrown" password="Hn3p#sR7"/>
    <item title="www.airbnb.com" login="lisa.johnson" password="e7Tf!kM2"/>
  </block>
  <block title="www.mail.ru" login="work" password="f8Sx%3dC">
    <item title="www.github.com" login="sarahjones " password="Wm9t@bD6"/>
  </block>
</root>

Этот файл и будет загружаться и выгружаться из облака. Интерфейс приложений будет так же повторять блочную структуру. Само приложение будет состоять из модулей: главный фрейм, модуль работы с XML, модуль шифрования/расшифровывания, для приложения на компьютере – модуль работы с java, модуль работы с облаком.

Алгоритм

Основной алгоритм по пунктам выглядит следующим образом:

  • Загрузка файла из облака

  • Загрузка файла с компьютера в память

  • Дешифрование файла

  • Отображение в интерфейсе

  • Шифрование файла

  • Сохранение файла из памяти на компьютере

  • Сохранение файла в облако

Подготовка

Первое, что необходимо сделать — это соответственно получить доступ к API-диска и настроить библиотеки.

Для работы с API-диска требуется логин и пароль, но вместо того, чтобы каждый раз вводить их, был перехвачен токен авторизации и использовался для работы с облаком. Чтобы это сделать необходимо скомпилировать пример, предоставленный разработчиками и сохранить токен для дальнейшего использования.

Скачать и скомпилировать библиотеку wxWidgets.

Скачать и скомпилировать библиотеку OpenSSL.

Скачать JDK.

Дальше последовательно и структурировано раскручивается клубок работы сервиса, проходя по каждому модулю программы, начиная снизу и двигаясь вверх. А именно модуль работы с облаком, модуль работы с java (для компьютера), модуль шифрования, модуль работы с XML, модуль интерфейса.

Клиентское приложение на компьютер

Модуль работы с облаком

Для компьютера модуль работы с облаком написан на языке программирования java, так как API-диска написаны на этом языке. JDK версии 18.0.2. Использовалось IDE IntelliJ IDEA. Сделан jar-архив, который будет условной библиотекой.

Создание проекта java-lib-password-manager:

Копируется исходник API-диска в проект в папку src начиная с папки com. Чтобы не менять ничего в исходниках, структура папок должна сохраняться. На всякий случай выкачены рабочие исходники, на момент написании статьи.

Подключение в проект необходимых библиотек: File → Project Structure… → вкладка Libraries → плюс → Java → выбираются необходимые библиотеки и дается согласие на добавление в выбранный модуль.

Библиотеки:

  • commons-logging-1.2.jar

  • httpclient-4.5.13.jar

  • httpcore-4.4.15.jar

  • xmlpull-1.1.3.4a.jar

Написано с версиями на случай, если с другими версиями что-то не заработает.

Сам проектный файл формата *.iml не заливается, так как при удалении временных файлов в папке .idea и самой папки, проектный файл невозможно правильно подключить. Вместо него есть makefile с помощью которого можно собрать проект, необходимо только указать правильный ${ROOT} путь.

Далее пишется сам код. В файле CloudLib.java создается класс CloudLib. В нем два публичных метода: load и upload.

CloudLib.java

public static boolean load(final String token,
                           final String remoteFullPath,
                           final String localFullPath) {

    CDownloadFileRetained load = new CDownloadFileRetained();

    return load.loadFile(token, remoteFullPath, localFullPath);
}

public static boolean upload(final String token,
                             final String remoteFullPath,
                             final String localFullPath) {

    CUploadFileRetained upload = new CUploadFileRetained();

    return upload.uploadFile(token, remoteFullPath, localFullPath);
}

В них создаются классы для работы с облаком, а так же передается токен API-диска, путь, где лежит/будет лежать файл на удаленном компьютере, путь, где лежит/будет лежать файл на локальном компьютере. Токен хранится в jar файле не будет, так как java язык легко дизассемблируемый.

Далее создаются два класса CDownloadFileRetained и CUploadFileRetained, которые наследуются от ProgressListener. Для этого необходимо переопределить два метода. С помощью этих двух методов можно определить статус загрузки и отменена ли загрузка. Для примера эти методы не используются. Так же у этих классов будут методы обработки запросов к облаку: loadFile и uploadFile соответственно.

@Override
public void updateProgress(final long loaded, final long total) {
}

@Override
public boolean hasCancelled() {
    return false;
}

Основные процессы происходят в методах loadFile и uploadFile соответствующих классов.

Первое что надо сделать – это создать объект Credentials передав туда параметры user и token.  User необязательный, его можно оставить пустым.

new Credentials("", token)

С помощью класса TransportClient происходит процесс загрузки или скачивания файлов. Для скачивания файла у этого класса необходимо вызвать метод getInstance передав туда Credentials, а для загрузки метод getUploadInstance с тем же параметром Credentials. Затем вызвать downloadFile или uploadFile.

При вызове downloadFile передаются параметры: путь куда файл будет загружаться, сам файл и сам ProgressListener. Файл загружается перед вызовом функции, если его нет, то пользователь увидит ошибку.

client = TransportClient.getInstance(new Credentials("", token));
client.downloadFile(remoteFullPath, file, CDownloadFileRetained.this);

Для uploadFile необходимы параметры локального и удаленного пути, а также ProgressListener.

client = TransportClient.getUploadInstance(new Credentials("", token));
client.uploadFile(localFullPath, remoteFullPath, CUploadFileRetained.this);

По завершению обмена данных необходимо разорвать соединение

CloudLib.java

TransportClient.shutdown(client);

Дальше всё это обворачивается в try catch и самое главное – это вызывать API-диска в отдельном потоке. Если вызывать напрямую, то ничего не заработает. Так же добавлена проверку результата, а после завершения потока, результат возвращается обратно в программу.

Код целиком
import com.yandex.disk.client.Credentials;
import com.yandex.disk.client.TransportClient;
import com.yandex.disk.client.ProgressListener;
import com.yandex.disk.client.exceptions.WebdavException;
import com.yandex.disk.client.exceptions.CancelledDownloadException;
import com.yandex.disk.client.exceptions.CancelledUploadingException;

import java.io.File;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicBoolean;

public class CloudLib {

    /** PUBLIC */

    public static boolean load(final String token,
                               final String remoteFullPath,
                               final String localFullPath) {

        CDownloadFileRetained load = new CDownloadFileRetained();

        return load.loadFile(token, remoteFullPath, localFullPath);
    }

    public static boolean upload(final String token,
                                 final String remoteFullPath,
                                 final String localFullPath) {

        CUploadFileRetained upload = new CUploadFileRetained();

        return upload.uploadFile(token, remoteFullPath, localFullPath);
    }

    /** PRIVATE */

    private static class CDownloadFileRetained
      implements ProgressListener {

        public boolean loadFile(final String token,
                                final String remoteFullPath,
                                final String localFullPath) {

            AtomicBoolean result = new AtomicBoolean(false);

            Thread thr = new Thread(() -> {

                TransportClient client = null;

                try {

                    File file = new File(localFullPath);
                    if(file.exists()) {
                        result.set(file.delete());
                        if(!result.get())
                            throw new IOException();
                    }

                    result.set(file.createNewFile());
                    if (!result.get())
                        throw new IOException();

                    client = TransportClient.getInstance(
                      new Credentials("", token)
                    );

                    client.downloadFile(remoteFullPath,
                                        file,
                                        CDownloadFileRetained.this
                                       );

                    result.set(true);

                    System.out.println("load: success");
                } catch (CancelledDownloadException ex) {
                    System.out.println("load: cancelled");
                } catch (IOException | WebdavException exception) {
                    System.out.println("load: error");
                    exception.printStackTrace();
                } finally {
                    TransportClient.shutdown(client);
                }
            });

            thr.start();

            try {
                thr.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
                result.set(false);
            }

            return result.get();
        }

        @Override
        public void updateProgress(final long loaded,
                                   final long total) {
        }

        @Override
        public boolean hasCancelled() {
            return false;
        }
    }

    private static class CUploadFileRetained
      implements ProgressListener {

        public boolean uploadFile(final String token,
                                  final String remoteFullPath,
                                  final String localFullPath) {

            AtomicBoolean result = new AtomicBoolean(false);

            Thread thr = new Thread(() -> {

                TransportClient client = null;

                try {
                    client = TransportClient.getUploadInstance(
                      new Credentials("", token)
                    );

                    client.uploadFile(localFullPath,
                                      remoteFullPath,
                                      CUploadFileRetained.this
                                     );

                    result.set(true);

                    System.out.println("upload: success");
                } catch (CancelledUploadingException ex) {
                    System.out.println("upload: cancelled");
                } catch (IOException | WebdavException exception) {
                    System.out.println("upload: error");
                    exception.printStackTrace();
                } finally {
                    TransportClient.shutdown(client);
                }
            });

            thr.start();

            try {
                thr.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
                result.set(false);
            }

            return result.get();
        }

        @Override
        public void updateProgress(final long loaded,
                                   final long total) {
        }

        @Override
        public boolean hasCancelled() {
            return false;
        }
    }
}

Дальше собираем jar-файл: File → Project Structure… → вкладка Artifacts → плюс → JAR → From modules with dependencies…

Откроется окно Create JAR from Modules.

Main Class – это точка входа, оставляется пустой, так как делается аналог библиотеки.

JAR files from library – это выбор между упаковать все необходимые элементы в один архив или скопировать их рядом

extract to the target JAR – собрать всё в один архив

copy to the output director and link via manifest – скопировать необходимые зависимости рядом.

Выбирается второй пункт, для чистоты эксперимента. Так же будет создан манифест в указанную папку, который не понадобится. Для подключения к С++ программе этого достаточно.

Дальше нажимается кнопка ОК.

В поле Output Layout нажимается правой кнопкой по наименованию jar-файла и изменяется имя  на “cloud-lib”

Enter, затем Apply, затем OK

Дальше Build → Build Artifacts… → Build

Должен появиться файл cloud-lib.jar вместе с другими архивами в папке:

C:/PasswordManager/java-lib-password-manager/out/artifacts/java_lib_password_manager_jar/

Это те файлы, которые будут подключаться к программе клиенту.

Для проверки работоспособности, производилась загрузка и сохранение XML-файла из облака, в облако. Для этого в этом же проекте создана мини программа, которая вызывает созданные методы.

CloudApp.java

public class CloudApp {

    public static void main(String[] args) {

        String token = null;
        String remotePath = null;
        String localPath = null;

        if(args.length == 4) {

            token = args[1];
            remotePath = args[2];
            localPath = args[3];

            switch(args[0]) {
                case "load" -> CloudLib.load(token, remotePath, localPath);
                case "upload" -> CloudLib.upload(token, remotePath, localPath);
                default -> System.out.println("no command");
            }
        }
    }
}

Соответственно по полученным аргументам: наименование метода, токен, удаленный путь, локальный путь, и вызывается соответствующий метод с параметрами <name method> <token> <remote path> <local path>.

Чтобы в IntelliJ IDEA, запустить приложение с параметрами необходимо создать новую конфигурацию, если она еще не создана, для этого в правом верхнем углу, рядом с молоточком, необходимо выбрать выпадающий список и в нем нажать Edit Configuration…

Дальше создаются две конфигурации: для загрузки и сохранения.

В открывшемся окне нажать на плюс → Application. Два раза для добавления второй конфигурации

Дальше вводятся необходимые параметры. Меняется Name на «cloud-app-load» и «cloud-app-upload». В Main class выбирается точка входа CloudApp. В Program arguments прописываются аргументы:

Для load:

"load" "<token>" "test/keys.xml" "C:/PasswordManager/resources/test/temp.txt"

Для upload:

"upload" "<token>" "test" "C:/PasswordManager/resources/keys.xml"

Дальше нажимается Apply, затем OK.

В этих случаях созданы тестовые папки. Если API-диска не найдёт какой-либо путь, то выдаст ошибку:

upload: error

com.yandex.disk.client.exceptions.IntermediateFolderNotExistException: Parent folder not exists for 'test'

load: error

com.yandex.disk.client.exceptions.RemoteFileNotFoundException: error while downloading file https://webdav.yandex.ru:443/test/keys.xml

Так же обратите внимание, что для load в localPath необходимо указать путь с именем файла. Последний элемент после слеша – это то куда будут записаны данный. Для upload в remotePath необходимо указать только путь.

Так же это мини приложение можно собрать в архив со всеми зависимостями и через консоль вызывать с соответствующими командами:

java -jar cloud-app.jar "upload" "<token>" "test" "C:/PasswordManager/resources/keys.xml"

java -jar cloud-app.jar "load" "<token>" "test/keys.xml" "C:/PasswordManager/resources/test/temp.txt"

Еще чтобы убедиться, что файл загружен в облако и открыть его в самом браузере можно сменить расширение файла с keys.xml на keys.txt.

Модуль работы с java

Дальше написана программа «cxx-wx-app-password-manager» на С++ которая вызывает написанный выше модуль.

Первое – это в проектном файле CMakeLists.txt задается путь к директории и путь к ресурсам проекта в виде макросов, к которым будет обращение из разного места кода программы:

CMakeLists.txt

указание переменной:

set(ROOT "C:/PasswordManager")Дальше есть два пути подключения JNI к C++ программе – это статическая или динамическая линковка jvm.dll библиотеки. При статической линковке необходимо, чтобы исполняемый файл лежал вместе с самой библиотекой по пути ${JDK}/bin/server. В этом варианте в конечной 
set(RESOURCES "${ROOT}/resources")

указание макроса:

add_definitions(-DRESOURCES="${RESOURCES}")

Дальше есть два пути подключения JNI к C++ программе – это статическая или динамическая линковка jvm.dll библиотеки. При статической линковке необходимо, чтобы исполняемый файл лежал вместе с самой библиотекой по пути ${JDK}/bin/server. В этом варианте в конечной программе можно вытащить ярлык программы на уровень выше. При динамической линковки конечная программа должна ссылаться на jvm.dll, то есть, в коде программы должен быть прописан абсолютный путь до jvm.dll. Так же в обоих случаях необходимо прописать абсолютные пути до вспомогательных *.jar библиотек. Чтобы описать оба метода подключения был введен макрос в cmake-файле STATIC_JDK_LINK:

set(STATIC_JDK_LINK OFF)

Так как предпочтительно использовать загрузку с помощью динамической линковки, а именно потому что остальная часть программы сможет работать, если не найдется необходимая библиотека во время запуска программы, то вводится макрос, указывающий на включение/выключение статической линковки.

Если макрос включен,

if(STATIC_JDK_LINK MATCHES ON)

то в проектный файл и соответственно в саму программу будет добавлен макрос STATIC_JDK_LINK,

add_definitions(-DSTATIC_JDK_LINK)

а также пути до библиотеки jvm.dll, куда cmake соберет исполняемый файл.

set(OUTPUT_DIRECTORY ${RESOURCES}/jdk-lite/bin/server)

set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${OUTPUT_DIRECTORY})
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${OUTPUT_DIRECTORY})
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${OUTPUT_DIRECTORY})

В данном примере указан путь jdk-lite. jdk-lite – это минимально необходимые файлы для работы виртуальной машины для конкретного примера, залитый в репозиторий.

Если же макрос STATIC_JDK_LINK выключен, то будет добавлен макрос

add_definitions(-DDYNAMIC_JDK_LINK)

а jvm.dll будет загружаться динамически.

Далее в проектном файле указываются пути до JDK, для подключения заголовочных файлов:

include_directories($ENV{JDK}/include)
include_directories($ENV{JDK}/include/win32)

Так же для статической линковки необходимо указать директорию и саму библиотеку JDK разработки, которая называется jvm.lib

if(STATIC_JDK_LINK MATCHES ON)
    link_directories($ENV{JDK}/lib)
endif()
if(STATIC_JDK_LINK MATCHES ON)
    target_link_libraries(${OUTPUT_NAME} jvm)
endif()

Дальше создаются Файлы jnicloudlib.h и jnicloudlib.cpp в которых создается класс CJNICloudLib.

В jnicloudlib.h подключают заголовочные файлы:

jnicloudlib.h

jni.h для работы с JVM

#include <jni.h>

и windows.h для динамической загрузки библиотеки

#ifdef DYNAMIC_JDK_LINK
#include <windows.h>
#endif // DYNAMIC_JDK_LINK

В самом классе CJNICloudLib будут переменные:

  • JavaVM – указатель на структуру, с помощью которой происходит управление виртуальной машиной (создание JNI_CreateJavaVM, удаление DestroyJavaVM), а также после создания виртуальной машины возвращается структуру JNIEnv;

  • JNIEnv – указатель на структуру, представляет интерфейс между нативным кодом и виртуальной средой, с помощью которого происходит вызов объектов класса, методов и переменных;

  • HINSTANCE – указатель на динамическую библиотеку;

И 2 метода:

  • Метод инициализации JVM

void init();
  • Метод вызова функции из CloudLib.jar

void run(const char* nameMethod,
         const char* token,
         const char* remotePath,
         const char* localPath
        );

В этот метод передаются такие же параметры, как и в CloudLib.jar, а именно имя метода («load» или «upload»), токен, удаленный и локальные пути.

Код целиком
#pragma once

#include <jni.h>

#ifdef DYNAMIC_JDK_LINK
#include <windows.h>
#endif // DYNAMIC_JDK_LINK

class CJNICloudLib
{
private:
    JavaVM* mJavaVM;
    JNIEnv* mJNIEnv;

#ifdef DYNAMIC_JDK_LINK
    HINSTANCE mInstance;
#endif // DYNAMIC_JDK_LINK

    void exception(const char* message);

public:
    CJNICloudLib();
    ~CJNICloudLib();

    void init();
    void run(const char* nameMethod,
             const char* token,
             const char* remotePath,
             const char* localPath);
};

В конструкторе класса переменные просто инициализируется нулями

jnicloudlib.cpp

    mJavaVM = nullptr;
    mJNIEnv = nullptr;

#ifdef DYNAMIC_JDK_LINK
    mInstance = nullptr;
#endif // DYNAMIC_JDK_LINK

Объект mJavaVM, связанный с виртуальной машиной создается и удалять только один раз, и живет на протяжении всей программы, так как после удаления DestroyJavaVM, виртуальную машина больше не получилось запустить, плюс не будет тратиться время при каждом запросе на создание виртуальной машины.

В методе init создается виртуальная машина. Для этого заводится сначала тип функции. Функция, которая будет вызываться и создавать виртуальную машину.

#ifdef DYNAMIC_JDK_LINK
typedef jint(JNICALL *pCreateJavaVM)(JavaVM**, void**, void*);
#endif // DYNAMIC_JDK_LINK

Дальше настройка виртуальную машину происходит с помощью параметров JavaVMInitArgs

JavaVMInitArgs vm_args;

Это структура с аргументами, которая передается в виртуальную машину и JavaVMOption

JavaVMOption options[1];

Структура, в которой передаются опции командной строки в виртуальную машину. Туда закидываются пути до необходимых библиотек для работы модуля работы с облаком.

const char* path = "-Djava.class.path="
                   PATH_LIB"/cloud-lib.jar;"
                   PATH_LIB"/commons-logging-1.2.jar;"
                   PATH_LIB"/httpclient-4.5.13.jar;"
                   PATH_LIB"/httpcore-4.4.15.jar;"
                   PATH_LIB"/xmlpull-1.1.3.4a.jar;";

options[0].optionString = const_cast<char*>(path);

Дальше заполняется JavaVMInitArgs параметрами:

vm_args.version = JNI_VERSION_1_8;
vm_args.nOptions = 1;
vm_args.options = options;

Здесь указывается версия JNI, параметры структуры JavaVMOption, а именно количество и сами опции.

Если совсем ничего не заработает можно указать: игнорировать нераспознанные опции командной строки.

vm_args.ignoreUnrecognized = JNI_TRUE;

Дальше для динамической линковки необходимо загрузить библиотеку из самого JDK и получить указатель на библиотеку.

mInstance = LoadLibrary(JDK "/bin/server/jvm.dll");

Дальше получение указателя на функцию JNI_CreateJavaVM из подключаемой библиотеки с помощью GetProcAddress:

JNI_CreateJavaVM = (pCreateJavaVM)GetProcAddress(mInstance, "JNI_CreateJavaVM");

И в конце, одинаково и для динамической и статической линковки, вызов функции JNI_CreateJavaVM, передав туда три параметра: JavaVM* и JNIEnv* и аргументы.

iResult = JNI_CreateJavaVM(&mJavaVM, (void**)&mJNIEnv, &vm_args);

Теперь самое муторное – всё это скомпилировать и запустить. Первое – это то, что есть вероятность не увидеть в консоли вывод приложения на С++. Второе — это то, что в debug режиме, в некоторых IDE приложение выбивает при создании виртуальной машины или обращению к ней, то есть, есть вероятность, что никакого отладчика здесь не будет. Третье – это то, что при вызове «создании виртуальной машины», при неправильной инициализации приложение всё равно выбивает, следовательно и ошибку, по которой JVM не создалась придется искать самому. Поэтому запускается всё на ощупь, например, если программа завершилась с ошибкой значит, что-то пошло не так. Плюс ко всему если выполнена успешная инициализация, то IDE может зависнуть. В общем, самое главное – это убедиться в правильности прописанных путей и всё заработает.

По-хорошему, дальше, для проверки работоспособности, необходимо написать функцию, которая просто вызывалась бы из нашего «модуля работы с облаком» и выводила информацию в консоль, но так как вызов простой функции из java и нашей отличается только тем, что необходимо добавить параметры вызова, то сразу продолжим писать код под необходимые потребности, а именно функцию вызова функций из «модуля работы с облаком»

Он же метод run и передача параметров: наименование метода, токен, локальный и удаленный пути:

bool CJNICloudLib::run(const char* nameMethod,
                       const char* token,
                       const char* localPath,
                       const char* remotePath
                      )

Здесь потребуются переменные JVM

Аргументы, которые передаются в вызываемый метод:

jstring arg[3];

Результат выполнения метод:

jboolean bResult = 0;

Объект класса и метод этого класса

jclass objClass = nullptr;
jmethodID objMethod = nullptr;

Дальше описываются сигнатуры, по которым определяются типы передаваемых параметров в вызываемую функцию. А именно передаются три переменных типа string и возвращаемое значение типа bool (сокращенное Z):

const char* signMethod = 
  "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Z";

Дальше находится вызываемый класс:

objClass = mJNIEnv->FindClass("CloudLib");

В случае успеха вернется указатель на класс, иначе nullptr.

Дальше получается метод класс:

objMethod = mJNIEnv->GetStaticMethodID(objClass, nameMethod, signMethod);

Так же проверяется на возвратное значение.

И последнее, вызывается сам метода, перед этим, преобразовывая С++-переменные в JNI-переменные и отправляя их в качестве аргументов:

arg[0] = mJNIEnv->NewStringUTF(token);
arg[1] = mJNIEnv->NewStringUTF(localPath);
arg[2] = mJNIEnv->NewStringUTF(remotePath);

resVal = mJNIEnv->CallBooleanMethod(objClass, objMethod, arg[0], arg[1], arg[2]);

Результат выполнения функции возвращаем в программу.

При необходимости можно использовать метод exception

void CJNICloudLib::exception(const char* message)

который будет проверять работу JVM-интерфейса и выдавать заданное исключение:

if(jniEnv->ExceptionCheck()) {
    jniEnv->ExceptionDescribe();
    jniEnv->ExceptionClear();
}

throw message;

Так в деструкторе завершаем работу JVM

if(mJavaVM != nullptr)
    mJavaVM->DestroyJavaVM();

и освобождаем из памяти загруженную библиотеку

#ifdef DYNAMIC_JDK_LINK
    if(mInstance != nullptr)
        FreeLibrary(mInstance);
#endif // DYNAMIC_JDK_LINK
Код целиком
#include "jnicloudelib.h"

#define PATH_LIB RESOURCES"/library"

#ifdef DYNAMIC_JDK_LINK
typedef jint(JNICALL *pCreateJavaVM)(JavaVM**, void**, void*);
#endif // DYNAMIC_JDK_LINK

CJNICloudLib::CJNICloudLib()
{
    mJavaVM = nullptr;
    mJNIEnv = nullptr;

#ifdef DYNAMIC_JDK_LINK
    mInstance = nullptr;
#endif // DYNAMIC_JDK_LINK
}

CJNICloudLib::~CJNICloudLib()
{
    if(mJavaVM != nullptr)
        mJavaVM->DestroyJavaVM();

#ifdef DYNAMIC_JDK_LINK
    if(mInstance != nullptr)
        FreeLibrary(mInstance);
#endif // DYNAMIC_JDK_LINK
}

void CJNICloudLib::init()
{
    jint iResult = 0;
    JavaVMInitArgs vm_args;
    JavaVMOption options[1];
#ifdef DYNAMIC_JDK_LINK
    pCreateJavaVM JNI_CreateJavaVM = nullptr;
#endif // DYNAMIC_JDK_LINK

    const char* path = "-Djava.class.path="  //
                       PATH_LIB "/cloud-lib.jar;"           //
                       PATH_LIB "/commons-logging-1.2.jar;" //
                       PATH_LIB "/httpclient-4.5.13.jar;"   //
                       PATH_LIB "/httpcore-4.4.16.jar;"     //
                       PATH_LIB "/xmlpull-1.1.3.4a.jar;";

    options[0].optionString = const_cast<char*>(path);

    vm_args.version = JNI_VERSION_1_8;
    vm_args.nOptions = 1;
    vm_args.options = options;
    // vm_args.ignoreUnrecognized = JNI_TRUE;

#ifdef DYNAMIC_JDK_LINK
    mInstance = LoadLibrary(JDK "/bin/server/jvm.dll");
    if(mInstance == nullptr)
        throw "error: library Java VM not load";

    JNI_CreateJavaVM = (pCreateJavaVM)GetProcAddress(
      mInstance, "JNI_CreateJavaVM");
    if(JNI_CreateJavaVM == nullptr)
        throw "error: function Java VM not find";
#endif // DYNAMIC_JDK_LINK

    iResult = JNI_CreateJavaVM(&mJavaVM, (void**)&mJNIEnv, &vm_args);
    if(iResult != 0)
        throw "Java VM not create";
}

void CJNICloudLib::run(const char* nameMethod,
                       const char* token,
                       const char* remotePath,
                       const char* localPath)
{
    jstring arg[3];
    jboolean bResult = 0;
    jclass objClass = nullptr;
    jmethodID objMethod = nullptr;

    const char* signMethod =
      "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Z";

    if(mJNIEnv == nullptr)
        throw "error: Java VM is not init";

    objClass = mJNIEnv->FindClass("CloudLib");
    if(objClass == nullptr)
        throw "error: java class is not find";

    objMethod = mJNIEnv->GetStaticMethodID(objClass,
                                           nameMethod,
                                           signMethod);
    if(objMethod == nullptr)
        throw "error: java method is not find";

    arg[0] = mJNIEnv->NewStringUTF(token);
    arg[1] = mJNIEnv->NewStringUTF(remotePath);
    arg[2] = mJNIEnv->NewStringUTF(localPath);

    bResult = mJNIEnv->CallBooleanMethod(objClass,
                                         objMethod,
                                         arg[0],
                                         arg[1],
                                         arg[2]);
    if(bResult != true)
        throw "error: java method is not success";
}

void CJNICloudLib::exception(const char* message)
{
    if(mJNIEnv->ExceptionCheck()) {
        mJNIEnv->ExceptionDescribe();
        mJNIEnv->ExceptionClear();
    }

    throw message;
}

Для проверки загружается тестовый файл в облако и сохраняется из облака. Для этого написана небольшая программа:

Main.cpp define 0:

#include <iostream>

#include "jnicloudelib.h"

int main(int argc, char** argv)
{
    CJNICloudLib cloudLib;

    try {
        cloudLib.init();

        cloudLib.run("upload", "<token>", "test",
                     RESOURCES "/keys.xml");

        cloudLib.run("load", "<token>", "test/keys.xml",
                     RESOURCES"/test/keys.xml");

    } catch(const char* message) {
        std::cout << message << std::endl;
    }

    return 0;
}

Помним, что лучше всего запускать напрямую через exe-файл. А также в случае этого примера положить скомпилированную библиотеку «cloud-lib» в папку вместе с другими java-библиотеками по пути:

C:/PasswordManager/resources/library

Дальше переходим к самому интересному.

Модуль шифрования

Вообще будут использоваться 2 метода шифрования. Первый (PBKDF2) восстановит из простого пароля необходимый ключ шифрования, а второй метод (AES-*) с помощью ключа шифрования будет работать непосредственно с файлом.

 Для первого шифрования будет использоваться массив рандомно сгенерированных данных из 256 байт (соль), для второго, массив из 16 байт (вектор инициализации).

Чтобы сгенерировать их воспользуемся непосредственно OpenSSL.

Команды в консоли openssl rand 256 > salt и openssl rand 16 > iv

создадут один и второй файлы с рандомно сгенерированными байтами.

Полученный результат:

Далее полученные файлы копируются из общих ресурсов в ресурсы директории проекта на С++, чтобы работать из-под этого проекта.

Команды:

copy iv C:\PasswordManager\cxx-wx-app-password-manager\res

copy salt C:\PasswordManager\cxx-wx-app-password-manager\res

и переходи в папку c ресурсам проекта C++

cd C:\PasswordManager\cxx-wx-app-password-manager\res

Чтобы напрямую работать с массивом данными и не подгружать их каждый раз из файлов, эти массивы сразу импортируются в исполняемый файл. Для этого полученные файлы преобразовываются в объектные файлы, с помощью инструменты «objcopy» (инструмент поставляется вместе с MinGW), а потом линкуются в exe-файл.

Для примера, чтобы понять, что происходит внутри объектного файла, создается тестовый файл test.txt внутри которого записано «Hello Habr» и из этого файла создаются объектные файлы, выполнив команды в консоли:

для х64:

objcopy --input-target binary --output-target elf64-x86-64 --binary-architecture i386:x86-64 test.txt test64.o

для х32:

objcopy --input-target binary --output-target elf32-i386 --binary-architecture i386 test.txt test32.o

--input-target binary – указывает, что входной файл (test.txt) является бинарным файлом и будет обрабатываться как последовательность байтов

--output-target elf64-x86-64 (elf32-i386) – указывает, что выходной файл (test*.o) будет в формате ELF (Executable and Linkable Format) для 64-битной (32-битной) архитектуры.

ELF – это стандартный формат файла для исполняемых файлов, объектных файлов, общих библиотек и других видов файлов, используемых в Unix-подобных операционных системах.

--binary-architecture i386:x86-64 (i386) – указывает явно архитектуру системы для бинарного файла: 64-битная или 32-битная архитектура.

Полученные файлы:

Чтобы посмотреть содержимое, используются команды:

objdump -x test64.o

objdump -x test32.o

Здесь зеленным выделено наименование архитектуры файла. Красным – наименование переменных, к которым можно обращаться из программного кода.

Так же открыв сами файлы, можно увидеть такие же переменные вызова (выделено красным) и сам массив данных (выделено синим).

Здесь:

  • _binary_test_txt_start –первый символ массива

  • _binary_test_txt_end – последний символ массива

  • _binary_test_txt_size – размер массива в виде адреса

Чтобы подключить объектный файл в проект, его необходимо добавить в cmake-файле в секцию add_executable:

CMakeLists.txt

для x64:

add_executable(obj64

    src/main.cpp
    res/test64.o
)

для x32:

add_executable(obj32

    src/main.cpp
    res/test32.o
)

При необходимости для отладки и компиляции 32-битного объектного файла необходимо в проектном файле указать флаг «-m32» и использовать gdb32.exe

Здесь все три переменных извлекаются как unsigned char, однако к первым двум переменным можно обратиться напрямую и получить первый и последний элемент массива: «H» и «\0» соответственно. Но так как необходим весь массив, то из двух переменных получаем указатель на начало массива и работаем с ним:target_compile_options(obj32 PRIVATE -m32)
target_link_options(obj32 PRIVATE -m32)

Пример обращения к переменным внутри объектного файла для x64:

Main.cpp define 1

#include <iostream>

extern unsigned char _binary_test_txt_start;
extern unsigned char _binary_test_txt_end;
extern unsigned char _binary_test_txt_size;

int main(int argc, char** argv)
{
    unsigned char* pStart = &_binary_test_txt_start;
    unsigned char* pEnd = &_binary_test_txt_end;
    unsigned char* pSize = &_binary_test_txt_size;

    size_t size = reinterpret_cast<uintptr_t>(pSize);

    std::cout << pStart << std::endl;
    std::cout << pEnd << std::endl;
    std::cout << size << std::endl;
    return 0;
}

Здесь все три переменных извлекаются как unsigned char, однако к первым двум переменным можно обратиться напрямую и получить первый и последний элемент массива: «H» и «\0» соответственно. Но так как необходим весь массив, то из двух переменных получаем указатель на начало массива и работаем с ним:

unsigned char* pStart = &binary_test_txt_start;

Отдельно, размер массива – это изначально адрес, даже несмотря на то, что он извлекается как unsigned char, к нему нельзя обратиться напрямую или разыменовать. Чтобы получить размер необходимо получить адрес и привести к числу, для это используем reinterpret_cast.

Здесь размер отобразился правильно, но это не значит, что он будет всегда правильным.

Пример обращения к переменным внутри объектного файла для x32:

Main.cpp define 2

#include <iostream>

extern unsigned char binary_test_txt_start;
extern unsigned char binary_test_txt_end;
extern unsigned char binary_test_txt_size;

int main(int argc, char** argv)
{
    unsigned char* pStart = &binary_test_txt_start;
    unsigned char* pEnd = &binary_test_txt_end;
    unsigned char* pSize = &binary_test_txt_size;

    uint16_t size0 = reinterpret_cast<uintptr_t>(pSize);
    size_t size1 = pEnd - pStart;

    std::cout << pStart << std::endl;
    std::cout << pEnd << std::endl;
    std::cout << size0 << std::endl;
    std::cout << size1 << std::endl;
    return 0;
}

Обратите внимание, что для 32-битного объектного файла, по умолчанию, не требуются подчеркивания для извлечения переменных, а размер так же отображается неправильно и, при приведении типов, необходимо выделить нужное число, либо использовать разность между адресами:

Так же есть возможность изменить имена переменных с помощью команды:

objcopy --redefine-sym _binary_test_txt_start=array_habr_start test64.o test64_.o

Результат:

Так же обратите внимание, что в 32-битном объектном файле, имя переменной должно начинаться с подчеркивания.

Дальше получены объектные файлы для соли (salt) и вектора инициализации (iv):

objcopy --input-target binary --output-target elf64-x86-64 --binary-architecture i386:x86-64 salt salt.o

objcopy --input-target binary --output-target elf64-x86-64 --binary-architecture i386:x86-64 iv iv.o

И подключение их к проектному файлу

    res/iv.o
    res/salt.o

Изменяться имена переменных не будут.

Дальше в проектный файл подключается библиотека OpenSSL.

CMakeLists.txt

Указывается путь до заголовочных файлов:

include_directories($ENV{OPENSSL}/include)

Указывается путь до библиотек:

link_directories($ENV{OPENSSL}/lib/gcc_libx64)

Подключаются сами библиотеки:

target_link_libraries(${OUTPUT_NAME} ssl)
target_link_libraries(${OUTPUT_NAME} crypto)
target_link_libraries(${OUTPUT_NAME} crypt32)
target_link_libraries(${OUTPUT_NAME} ws2_32)

Порядок библиотек необходимо сохранить.

Дальше создается класс CCipher, файлы cipher.h и cipher.cpp.

В нем будет соответственно два публичных метода: шифрование и дешифрование и еще три приватных методах, о которых будет рассказано дальше.

cipher.h

Шифрование:

static void encrypt(const char* password,
                    const uchar* inData,
                    const int& inSize,
                    uchar* outData,
                    uint& outSize);

Дешифрование:

static void decrypt(const char* password,
                    const uchar* inData,
                    const int& inSize,
                    uchar* outData,
                    uint& outSize);

В обоих методах потребуется пароль, входной и выходной массивы данных.

Пароль – это простой пароль, который пользователь сможет запомнить. В данном примере используется 4 цифры.

Метод восстановления ключа:

static void getKey(const char* password, const uchar* salt, uchar* key);

Внутренний метод шифрования, работающий с OpenSSL:

static void innerEncrypt(const uchar* inData,
    const int& inSize,
    const uchar* key,
    const uchar* iv,
    uchar* outData,
    uint& outSize);

Внутренний метод дешифрования, работающий с OpenSSL:

static void innerDecrypt(const uchar* inData,
    const int& inSize,
    const uchar* key,
    const uchar* iv,
    uchar* outData,
    uint& outSize);

В файле cpp подключается заголовочный файл для работы с OpenSSL:

cipher.cpp

#include <openssl/evp.h>

Дальше первое – это необходимо создать ключ шифрования. Для этого написан метод, он же метод восстановления ключа шифрования:

static void getKey(const char* password, unsigned char* salt, unsigned char* key);

Здесь тот же пароль из 4 чисел, соль и выходной ключ шифрования.

Внутри используется метод

int PKCS5_PBKDF2_HMAC(const char* pass,
                      int passlen,
                      const unsigned char* salt,
                      int saltlen,
                      int iter,
                      const EVP_MD* digest,
                      int keylen,
                      unsigned char* out);

PKCS5 (Public Key Cryptography Standards 5) – это значит, что алгоритм является частью стандарта PKCS #5

PBKDF2 (Password-Based Key Derivation Function 2) – это метод шифрования, используемый для генерации ключа из пароля (или другого секретного значения).

HMAC (Hash-based Message Authentication Code) – это алгоритм, используемый для проверки целостности и аутентичности сообщений.

В этот метод передаем пароль пользователя, соль, количество итераций, дайджест и выходной массив.

pass – пароль

passlen – размер пароля

salt – это рандомно сгенерированный массив байт, с помощью которого происходит восстановление или создание ключа шифрования.

Iter – количество итераций, которые будет выполнять алгоритм. Большее количество итераций увеличивает время, необходимое для генерации ключа, что делает его более безопасным против атак перебором.

digest – указатель на структуру, описывающую алгоритм хеширования, который будет использоваться в процессе генерации ключа. Функции, как EVP_sha256(), EVP_sha512(), и EVP_sha1() возвращают структуру, соответствующую алгоритму хеширования:

  • SHA-256 генерирует хеш-значение длиной 256 бит (32 байта) из входных данных.

  • SHA-512 генерирует хеш-значение длиной 512 бит (64 байта) из входных данных.

  • SHA-1 генерирует хеш-значение длиной 160 бит (20 байтов) из входных данных.

keylen – задает размер выходного массива

out – указатель на выходной массив

Как это работает: PBKDF2 генерирует ключ заданной длины из пароля и соли. Делается это путем многократного применения хеш-функции к сочетанию пароля, соли и номера итерации, затем производится конкатенация результатов. Продолжается это до тех пор, пока не будет сгенерирован ключ нужной длины.

В этот метод соответственно передаем пароль, размер пароля (константа), сгенерированную соль, размер соли (константа), количество итерация 1324, дайджест надежного хеширования SHA_512, размер ключа необходим 32 байта (об этом дальше) и указатель на массив.

int result = PKCS5_PBKDF2_HMAC(password,
                               SIZE_PASSWORD,
                               salt,
                               SIZE_SALT,
                               1324,
                               EVP_sha512(),
                               SIZE_KEY,
                               key);

Функция возвращает 1 в случае успеха и 0 если ошибка. Из функции возвращается массив с ключом.

Дальше идет основная работа с OpenSLL и написаны сразу два статических метода шифрования и дешифрования:

void CCipher::innerEncrypt(const uchar* inData,
                           const int& inSize,
                           const uchar* key,
                           const uchar* iv,
                           uchar* outData,
                           uint& outSize)
void CCipher::innerDecrypt(const uchar* inData,
                           const int& inSize,
                           const uchar* key,
                           const uchar* iv,
                           uchar* outData,
                           uint& outSize)

Так как они полностью выполняют одинаковые действия, получают и работают с одинаковыми переменными и вызывают методы с разными названиями, но идентичны по значению, то они описаны совместно друг с другом.

 В эти функции передаются текст шифрования/дешифрования, его размер, вектор инициализации, выходной массив и выходной размер.

Внутри методов первое что необходимо сделать – это создать контекст шифрования:

EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();

Эта функция одинакова для шифрования/дешифрования, создает и возвращает структуру EVP_CIPHER_CTX, которая будет содержать все необходимые данные для выполнения операций шифрования и дешифрования с использованием симметричных алгоритмов.

Дальше вызываем последовательно 3 метода, для каждого шифрования своё название:

  • EVP_EncryptInit_ex() или EVP_DecryptInit_ex() – инициализирует контекст шифрования в зависимости от указанного алгоритма, движка, вектора инициализации и ключа шифрования.

int EVP_EncryptInit_ex(EVP_CIPHER_CTX* ctx,
                       const EVP_CIPHER* type,
                       ENGINE* impl,
                       const unsigned char* key,
                       const unsigned char* iv);
int EVP_DecryptInit_ex(EVP_CIPHER_CTX *ctx,
                       const EVP_CIPHER *type,
                       ENGINE *impl,
                       const unsigned char *key,
                       const unsigned char *iv);

ctx – контекст шифрования. Контекст используется для хранения состояния шифрования, поэтому каждый раз необходимо создавать новый. Он используется для передачи информации о текущем состоянии шифрования.

type – алгоритм шифрования указывающий на структуру. Например методы EVP_aes_128_ctr(), EVP_aes_192_cfb(), EVP_aes_256_cbc() возвращают структуры метода AES шифрования с различными режимами работы:

  1. EVP_aes_128_ctr – AES в режиме CTR (Counter) с ключом 128 бит. Каждый блок данных шифруется с использованием счетчика, который увеличивается после каждой операции шифрования. Эффективен для большого объема данных, но не обеспечивает целостность сообщения;

  2. EVP_aes_192_cfb – AES в режиме CFB (Cipher Feedback) с ключом 192 бита. Каждый блок данных шифруется с использованием предыдущего блока данных, следовательно каждый блок шифротекста зависит от предыдущего блока, что обеспечивает целостность сообщения, но не эффективен для большого объема данных;

  3. EVP_aes_256_cbc – AES в режиме CBC (Cipher Block Chaining) с ключом длиной 256 бит. Каждый блок данных шифруется с использованием предыдущего блока шифротекста и вектора инициализации (IV). Первый блок данных шифруется с использованием IV, а каждый последующий блок шифруется с использованием предыдущего блока шифротекста. Не эффективен для большого объема данных, обеспечивает целостность сообщения;

impl – реализация шифрования: программное или аппаратное.  Если передать параметр NULL, то реализация по умолчанию будет программная с использованием методов OpenSSL и выполнением шифрования и дешифрования на процессоре устройства. Иначе указывается специализированное аппаратное обеспечение ENGINE *impl, которое значительно ускоряет процесс шифрования и дешифрования.

key – ключ шифрования. Длина ключа зависит от выбранного алгоритма шифрования. Например, для AES-128 ожидается, что ключ шифрования будет 128-битным или 16 байт. Для AES-256 соответственно 256-битным или 32 байта

iv – вектор инициализации (IV), если он требуется для алгоритма шифрования. Он должен быть 16 байт

  • EVP_EncryptUpdate() или EVP_DecryptUpdate() – шифрует блоки данных.

int EVP_EncryptUpdate(EVP_CIPHER_CTX *ctx,
                      unsigned char *out,
                      int *outl,
                      const unsigned char *in,
                      int inl);
int EVP_DecryptUpdate(EVP_CIPHER_CTX *ctx,
                      unsigned char *out,
                      int *outl,
                      const unsigned char *in,
                      int inl);

ctx – контекст шифрования

out – указатель на буфер, в который будет записан результат шифрования. Размер буфера должен быть достаточным для хранения зашифрованных данных. Размер зависит от размера входных данных и используемого алгоритма шифрования.

outl – размер зашифрованных данных, которые будут записаны в выходную переменную. Этот размер необходимо сохранить в отдельную переменную, так как это не полный размер и к этому размеру будет прибавляться размер оставшейся части данных.

in – указатель на буфер, содержащий данные, которые необходимо зашифровать.

inl – размер входных данных в байтах, указывает, сколько байтов из буфера in нужно зашифровать.

  • EVP_EncryptFinal_ex() или EVP_DecryptFinal_ex() – шифрует оставшиеся блоки данных.

int EVP_EncryptFinal_ex(EVP_CIPHER_CTX *ctx, unsigned char *out, int *outl);
int EVP_DecryptFinal_ex(EVP_CIPHER_CTX *ctx, unsigned char *outm, int *outl);

ctx – контекст шифрования;

out – указатель на буфер, в который будет записан результат шифрования. Размер буфера должен быть достаточным для хранения зашифрованных данных. Размер зависит от размера входных данных и используемого алгоритма шифрования;

outl – размер зашифрованных данных, которые будут записаны в выходную переменную. Это размер оставшейся части данных, который необходимо прибавить к предыдущему сохраненному размеру;

После выполнения шифрования необходимо освободить контекст методом:

EVP_CIPHER_CTX_free(ctx);

Дальше собирается всё в единый код и проверяется работоспособность:

Main.cpp define 3

#include <cstring>
#include <iostream>

#include "cipher.h"

#define SIZE_BUFFER 20

int main(int argc, char** argv)
{
    const char* password = "0011";

    uchar inData[SIZE_BUFFER] = { 0 };
    int inSize = SIZE_BUFFER;

    uchar encryptData[SIZE_BUFFER] = { 0 };
    uint encryptSize = SIZE_BUFFER;

    uchar outData[SIZE_BUFFER] = { 0 };
    uint outSize = SIZE_BUFFER;

    memcpy(inData, "Hello habr", inSize = 10);

    try {
        CCipher::encrypt(password, inData, inSize, encryptData, encryptSize);
        CCipher::decrypt(password, encryptData, encryptSize, outData, outSize);

        std::cout << "In     \tsize: " << inSize << " data: "<< inData
          << std::endl;
        std::cout << "Encrypt\tsize: " << encryptSize << " data: " << encryptData
          << std::endl;
        std::cout << "Out    \tsize: " << outSize << " data: " << outData
          << std::endl;

    } catch(...) {
    }

    return 0;
}

Модуль работы с XML

Дальше написан класс загрузки, сохранения шифрованного файла, а также работы с xml форматом. Для это будет использоваться класс и методы библиотеки wxWidgets.

Подключение библиотеки к cmake-файлу:

CMakeLists.txt

find_package(wxWidgets 3.2 REQUIRED core base xml)
include_directories(${wxWidgets_INCLUDE_DIRS})
link_directories(${wxWidgets_LIB_DIR})
target_link_libraries(${OUTPUT_NAME} ${wxWidgets_LIBRARIES})

При необходимости задать путь расположения библиотек:

set(wxWidgets_LIB_DIR $ENV{WXWIN}/lib/gcc_libx64)

Создается класс CXMLFile файлы xmlfile.h и xmlfile.cpp. У него будут методы:

xmlfile.h:

  • чтение:

static void read(const char* nameFile,
                 uchar* data,
                 unsigned int& size);
  • запись:

static void write(const char* nameFile,
                  const uchar* data,
                  const unsigned int& size);
  • разобрать xml-файл:

static void parse(unsigned char* inData,
                  const unsigned int& inSize,
                  TVectorItems& outData);
  • собрать xml-файл:

static void collect(const TVectorItems& inData,
                    uchar* outData,
                    unsigned int& outSize);

Так же был введен внутренний простой контейнер для передачи данных между классами – вектор, который содержит структуру с параметрами: тип, заголовок, логин, пароль:

struct SItem {
    std::string type;
    std::string title;
    std::string login;
    std::string password;
};

typedef std::vector<SItem> TVectorItems;

Чтение и запись будут производится через класс wxFile и возвращать/передавать сырые байты данных по наименованию файла. Здесь ничего не обычного и сделано по-простому: создается объект, открывается файл на чтение и запись соответственно, читается или записываем файл в зависимости от метода, закрывается файл:

xmlfile.cpp

void CXMLFile::read(const char* nameFile, uchar* data, unsigned int& size)
{
    wxFile file;
    bool bResult = false;

    bResult = file.Open(nameFile, wxFile::read);
    if(bResult == false)
        throw "File not open";

    size = file.Read(data, size);

    file.Close();
}
void CXMLFile::write(const char* nameFile,
                     const uchar* data,
                     const unsigned int& size)
{
    wxFile file;
    bool bResult = false;

    bResult = file.Open(nameFile, wxFile::write);
    if(bResult == false)
        throw "File not open";

    file.Write(data, size);

    file.Close();
}

Проверка:

Main.cpp define 4

#include "xmlfile.h"

#define SIZE_BUFFER 1000

int main(int argc, char** argv)
{
    uchar data[SIZE_BUFFER] = { 0 };
    unsigned int size = SIZE_BUFFER;

    CXMLFile::read(RESOURCES "/keys.xml", data, size);
    CXMLFile::write(RESOURCES "/temp.xml", data, size);
}

Если файл temp.xml создался и соответствует файлу keys.xml, то всё работает.

Дальше два метода разбора и сбора xml-файла, уникальны тем, что загрузка входных данных в методы классов «wxXml» происходит не из файла, а из потока данных, так как чувствительные данные необходимо держать только в оперативной памяти и никаких следов на жестком диске.

В методе parse создается класс wxXmlDocument и входной поток wxMemoryInputStream в которые передатся массив данных и его размер. Дальше поток загружается в wxXmlDocument с помощью метода Load и производится структурный анализ xml-файла. Так как структура хранения данных в wxXmlDocument – это дерево, то разбор производится средством обхода, а именно рекурсивным вызовом метода round в который передается узел и выходной контейнер, в который будут наполнятся данные:

void CXMLFile::parse(unsigned char* inData, const unsigned int& inSize, TVectorItems& outData)
{
    wxXmlDocument xmlDoc;
    wxMemoryInputStream stream(inData, inSize);

    if(xmlDoc.Load(stream) == false)
        throw "Raw data not load to xml";

    round(xmlDoc.GetRoot()->GetChildren(), outData);
}

В методе round проверяется входной узел на nullptr – это блокирующий фактор, который завершает обход дерева.

if(node != nullptr) {

Обходить дерево можно любым из трех способов: прямой, центрированный или обратный обход. В данном примере выбран прямой ход, и чтобы во внутреннем контейнере данные сохранялись так же, как читается xml-файл сверху вниз, то после обработки текущего узла обрабатываются дети затем следующие узлы. Обработка текущего узла заключается в получении атрибутов и сохранении их во внутренний контейнер. Атрибуты в памяти устроены как список, элементы которого ссылаются друг на друга. Обход можно так же произвести с помощью рекурсии, но для наглядности кода был использован цикл. Перед его вызовом создается новый элемент в векторе.

outData.emplace_back();

В новый элемент сразу устанавливается тип узла

outData.back().type = node->GetName();

Дальше используется указатель на текущий атрибут, который будет меняться с каждой итерацией.

attr = node->GetAttributes();

Внутри цикла происходит поиск необходимого атрибута по его имени, а его значение сохраняется в вектор. В конце цикла – переход к следующему атрибуту и его проверка на nullptr.

do {

    wxString name = attr->GetName();
    wxString value = attr->GetValue();

    if(name == wxT("title"))
        outData.back().title = value;

    if(name == wxT("login"))
        outData.back().login = value;

    if(name == wxT("password"))
        outData.back().password = value;

} while((attr = attr->GetNext()) != nullptr);

И вызов следующего узла:

round(node->GetChildren(), outData);

И вызов следующего узла:

round(node->GetNext(), outData);

В итоге получается наполненный вектор значениями из xml-файла:

Дальше противоположный процесс: перевести данные внутреннего контейнера в xml-формат и выдать массив данных в методе CXMLFile::collect. Здесь так же понадобятся wxXmlDocument и выходной поток wxMemoryOutputStream.

wxXmlDocument xmlDoc;
wxMemoryOutputStream stream;

Так же 3 переменные – указатели на узел xml, которые являются вложениями xml-структуры: «root», «block», «item»

wxXmlNode* root = nullptr;
wxXmlNode* block = nullptr;
wxXmlNode* item = nullptr;

Первое – необходимо создать главный узел «root», который будет родителем всех узлов:

wxXmlNode(wxXmlNodeType type,
          const wxString& name,
          const wxString& content = wxEmptyString,
          int lineNo = -1);

type – это тип узла. wxXML_ELEMENT_NODE – наиболее распространенный тип узла в XML-документе. Содержит начальный тег, содержимое и конечный тег;

name – имя узла;

Создание узла:

root = new wxXmlNode(wxXML_ELEMENT_NODE, wxT("root"));

Здесь и далее используется упрощенный конструктор, так как узел не будет привязан к родительскому узлу и его можно разместить в любой необходимой позиции xml-файла, иначе, если узел сразу привязывать к родительскому, то по умолчанию узлы будут расставлены в обратной последовательности.

Дальше в цикле обходим вектор и в зависимости от типа узла создаем «block»-узел или «item»-узел, устанавливаем атрибуты созданного узла и добавляем узел в родительские узел. Так же проверяется родительский узел на nullptr.

for(auto& p : inData) {

        if(root != nullptr && p.type == wxT("block")) {
            block = new wxXmlNode(wxXML_ELEMENT_NODE, wxT("block"));
            block->AddAttribute(wxT("title"), p.title);
            block->AddAttribute(wxT("login"), p.login);
            block->AddAttribute(wxT("password"), p.password);
            root->AddChild(block);
        }

        if(block != nullptr && p.type == wxT("item")) {
            item = new wxXmlNode(wxXML_ELEMENT_NODE, wxT("item"));
            item->AddAttribute(wxT("title"), p.title);
            item->AddAttribute(wxT("login"), p.login);
            item->AddAttribute(wxT("password"), p.password);
            block->AddChild(item);
        }
    }

Дальше сохраняется «root»-узел в wxXmlDocument

xmlDoc.SetRoot(root);

Сохраняется xml-структура в выходной поток

xmlDoc.Save(stream);

И выходной поток копируем в выходной буфер

stream.CopyTo(outData, outSize = stream.GetSize());

Проверка:

Main.cpp define 5

#include "xmlfile.h"

#define SIZE_BUFFER 1000

int main(int argc, char** argv)
{
    uchar dataRead[SIZE_BUFFER] = { 0 };
    unsigned int sizeRead = SIZE_BUFFER;

    TVectorItems items;

    uchar dataWrite[SIZE_BUFFER] = { 0 };
    unsigned int sizeWrite = SIZE_BUFFER;

    CXMLFile::read(RESOURCES "/keys.xml", dataRead, sizeRead);

    CXMLFile::parse(dataRead, sizeRead, items);

    CXMLFile::collect(items, dataWrite, sizeWrite);

    CXMLFile::write(RESOURCES "/temp.xml", dataWrite, sizeWrite);
}

После выполнения программы должны получиться 2 одинаковых файла:

Проверка модулей

Так как написаны все внутренние backend модули, то необходимо проверить их работоспособность и так же сохранить в облако шифрованный файл. Для этого написано консольное приложение, в которое подключаются модули CJNICloudLib, CCipher, CXMLFile, где производится загрузка файла в программу его шифрование, сохранение зашифрованного файла в облако, загрузка из облака, его расшифрование и сохранение на диск. То есть полная проверка цикла только в другой последовательности и без отображения в интерфейсе:

  • Загрузка файла с компьютера в память

  • Шифрование файла

  • Сохранение файла из памяти на компьютере

  • Сохранение файла в облако

  • Очистка буферов

  • Загрузка файла из облака

  • Загрузка файла с компьютера в память

  • Дешифрование файла

  • Сохранение файла из памяти на компьютере

На этом этапе вытащим токен в отдельный файл

и добавим его в проект через макрос:

CMakeLists.txt

file(READ "${RESOURCES}/token" TOKEN)
add_definitions(-DTOKEN="${TOKEN}")

И сама проверочная программа:

Main.cpp define 6

#include <iostream>

#include "xmlfile.h"
#include "jnicloudelib.h"
#include "cipher.h"

#define SIZE_BUFFER 8192

int main(int argc, char** argv)
{
    uchar dataDecrypt[SIZE_BUFFER] { 0 };
    uint sizeDecrypt = SIZE_BUFFER;

    uchar dataEncrypt[SIZE_BUFFER] { 0 };
    uint sizeEncrypt = SIZE_BUFFER;

    CJNICloudLib jniCloudLib;

    try {

        jniCloudLib.init();

        CXMLFile::read(RESOURCES "/keys.xml",
                       dataDecrypt,
                       sizeDecrypt);

        CCipher::encrypt("0011",
                         dataDecrypt,
                         sizeDecrypt,
                         dataEncrypt,
                         sizeEncrypt);

        CXMLFile::write(RESOURCES "/keys_encrypt_upload.xml",
                        dataEncrypt,
                        sizeEncrypt);

        jniCloudLib.run("upload",
                        TOKEN,
                        "test",
                        RESOURCES "/keys_encrypt_upload.xml");

        memset(dataDecrypt, 0, sizeDecrypt = SIZE_BUFFER);
        memset(dataEncrypt, 0, sizeEncrypt = SIZE_BUFFER);

        jniCloudLib.run("load",
                        TOKEN,
                        "test/keys_encrypt_upload.xml",
                        RESOURCES "/keys_encrypt_load.xml");

        CXMLFile::read(RESOURCES "/keys_encrypt_load.xml",
                       dataEncrypt,
                       sizeEncrypt);

        CCipher::decrypt("0011",
                         dataEncrypt,
                         sizeEncrypt,
                         dataDecrypt,
                         sizeDecrypt);

        CXMLFile::write(RESOURCES "/keys_decrypt.xml",
                        dataDecrypt,
                        sizeDecrypt);

    } catch(const char* message) {
        std::cout << message << std::endl;
    }
}

Здесь происходит чтение файла keys.xml из ресурсов, его шифрование и запись в файл keys_encrypt_upload.xml. Этот файл загружается в облако, затем очищаются буферы, и выгружается файл из облака как keys_encrypt_load.xml. Дальше чтение этого файла, расшифровка и запись как keys_decrypt.xml. Итого должны появиться еще 3 файла: одинаковые keys_encrypt_upload.xml, keys_encrypt_load.xml, и одинаковые keys.xml, keys_decrypt.xml.

Модуль интерфейса

Дальше написан простой интерфейс управления, в котором будут расположены 2 кнопки, одна из которых будет сохранять информацию с компьютера в облако, вторая – загружать из облака на компьютер и таблица для паролей. Кнопки управления будут расположены сверху. Таблица расположена под кнопками. Редактирование происходит путем изменения ячеек таблицы. Для этого написан класс CMainFrame, в котором инициализация интерфейса происходит в конструкторе, 2 метода OnLoad и OnUpload, а также переменная класса CJNICloudLib, вспомогательные буферы, вектор, и элементы wxWidgets, а именно wxGrid (таблица):

mainframe.h

#pragma once

#include <wx/frame.h>
#include <wx/grid.h>

#include "cipher.h"
#include "jnicloudelib.h"
#include "xmlfile.h"

#define SIZE_BUFFER 8192

class CMainFrame : public wxFrame
{
private:
    wxGrid* mGrid;

    TVectorItems mItems;

    uchar mDataEncrypt[SIZE_BUFFER];
    unsigned int mSizeEncrypt;

    uchar mDataDecrypt[SIZE_BUFFER];
    unsigned int mSizeDecrypt;

    // CJNICloudLib mJNICloudLib;

    void OnLoad(wxCommandEvent& event);
    void OnUpload(wxCommandEvent& event);

public:
    CMainFrame();
    ~CMainFrame() override = default;
};

Здесь подключаются необходимы зависимости. Класс CMainFrame наследуется от wxFrame, поэтому необходимо перезагрузить деструктор. Так же по правилам wxWidgets в методы OnLoad и OnUpload передается параметр события wxCommandEvent, без которого события не заработают. В них передаются параметры объекта, вызывающего событие.

Дальше простейшее определение класса, чтобы проверить работу интерфейса. Функционал будет добавляться на втором этапе:

mainframe.cpp

#include <wx/button.h>
#include <wx/sizer.h>

#define INDEX_LABEL_TITLE 0
#define INDEX_LABEL_LOGIN 1
#define INDEX_LABEL_PASSWORD 2
#define INDEX_LABEL_TYPE 3

CMainFrame::CMainFrame()
    : wxFrame(nullptr, NewControlId(), wxT("Password manager"))
{
    wxButton* btnLoad = new wxButton(this, NewControlId(), wxT("Load"));
    wxButton* btnUpload = new wxButton(this, NewControlId(), wxT("Upload"));

    mGrid = new wxGrid(this, NewControlId());
    mGrid->CreateGrid(0, 4);
    mGrid->SetColLabelValue(INDEX_LABEL_TITLE, "title");
    mGrid->SetColLabelValue(INDEX_LABEL_LOGIN, "login");
    mGrid->SetColLabelValue(INDEX_LABEL_PASSWORD, "password");
    mGrid->SetColLabelValue(INDEX_LABEL_TYPE, "type");
    mGrid->HideRowLabels();

    wxBoxSizer* hBox = nullptr;
    wxBoxSizer* mainBox = new wxBoxSizer(wxVERTICAL);

    hBox = new wxBoxSizer(wxHORIZONTAL);
    hBox->Add(btnLoad, 1);
    hBox->Add(btnUpload, 1);
    mainBox->Add(hBox, 0, wxEXPAND);
    mainBox->Add(mGrid);

    SetSizerAndFit(mainBox);

    Bind(wxEVT_BUTTON, &CMainFrame::OnLoad, this, btnLoad->GetId());
    Bind(wxEVT_BUTTON, &CMainFrame::OnUpload, this, btnUpload->GetId());
}

void CMainFrame::OnLoad(wxCommandEvent& WXUNUSED(event))
{
    int nRows = mGrid->GetNumberRows();
    mGrid->AppendRows();
    mGrid->SetCellValue(nRows, INDEX_LABEL_TITLE, "");
    mGrid->SetCellValue(nRows, INDEX_LABEL_LOGIN, "");
    mGrid->SetCellValue(nRows, INDEX_LABEL_PASSWORD, "");
    mGrid->SetCellValue(nRows, INDEX_LABEL_TYPE, "");
    this->Fit();
}

void CMainFrame::OnUpload(wxCommandEvent& WXUNUSED(event))
{
    int nRows = mGrid->GetNumberRows();
    for(int iRow = 0; iRow < nRows; iRow++) {

        mGrid->GetCellValue(iRow, INDEX_LABEL_TITLE);
        mGrid->GetCellValue(iRow, INDEX_LABEL_LOGIN);
        mGrid->GetCellValue(iRow, INDEX_LABEL_PASSWORD);
        mGrid->GetCellValue(iRow, INDEX_LABEL_TYPE);
    }
}

Здесь подключаются необходимы зависимости. Макросами обозначаются индексы лейблов колонок в таблице. Дальше в наследуемый класс wxFrame передается родитель (он ноль, так как это окно верхнего уровня), уникальный идентификатор, который создается и выдается помощью метода NewControlId() и лейбл программы. Дальше создание кнопок: в их конструктор передаются родитель (CMainFrame), идентификатор, лейбл кнопки. Создание таблицы: передаются родитель и идентификатор. Само создание таблицы происходит с помощью метода CreateGrid и передачей туда количества строк и колонок. Лейблы каждой колонки задаются отдельны по индексу с помощью метода SetColLabelValue. И в конце создания таблицы, скрываются вертикальные лейблы, так как они не нужны.

Дальше необходимо разместить элементы во фрейме. Для этого используются сайзеры, которые последовательно, друг за другом, как горизонтальный или вертикальный массив, располагают элементы в себе на форме в зависимости от типа сайзера. В этом приложении зайзеры выглядят так:

Оранжевым выделен фрейм, синим вертикальный сайзер (в коде – это mainBox), зеленым горизонтальный сайзер (в коде – это hBox). То есть создаются указатели hBox и mainBox, mainBox инициализируется как вертикальный сайзер и подчеркивается наименованием, что это сайзер верхнего уровня. Дальше инициализируется горизонтальный сайзер, который наполняется двумя кнопками с помощью функцией Add. И в конце расположения элементов в mainBox добавляется горизонтальный сайзер и элемент таблицы. Так же в функцию Add в данном примере передаются еще два параметра: proportion и flag. Первый параметр со значением 1 указывает возможность растягиваться по правилам родительского сайзера. То есть кнопки будут растянуты по горизонтали, так как добавлены в горизонтальный сайзер. А в сам горизонтальный сайзер добавляется с параметром 0 и флагом wxEXPAND, указывая, что его дети имеют возможность растягиваться.

По мимо вертикальных и горизонтальных, вообще есть табличные сайзеры или сайзеры с окаймовкой и текстом.

И в самом конце расположения элементов главный сайзер устанавливается во фрейм с помощью метода SetSizerAndFit

Дальше присоединим события к кнопкам с помощью метода Bind в который передаются: тип события (wxEVT_BUTTON – это событие нажатие кнопки), метод, который будет выполняться по события, объект к которому принадлежит метод события и уникальный идентификатор элемента во фрейме (В данном примере – это идентификатор кнопки). То есть по нажатию кнопки OnLoad или OnUpload сработает событие CMainFrame::OnLoad или CMainFrame::OnUpload.

В самих же методах обработка данных происходит следующим образом.

В OnLoad получается количество строк, добавляется новая строка, затем эта строка заполняется новыми элементами в конце подгоняем фрейм под размеры таблицы с помощью метода Fit.

В UpLoad получается количество строк и в цикле происходит обход всей таблицы.

Дальше необходимо загрузить фрейм. Для это в main.cpp создается класс CApp наследуемый от wxApp:

main.cpp define 7

#include <wx/app.h>

#include "mainframe.h"

class CApp : public wxApp
{
public:

    bool OnInit() override
    {
        return (new CMainFrame())->Show();
    }
};

IMPLEMENT_APP(CApp)

Здесь происходит создание фрейма по правилам wxWidgets. В созданном классе переопределяется метод OnInit, который запускается после запуска программы и инициализации самого wxWidgest. Внутри метода создается основной фрейм и показывается с помощью метода Show. В конце созданный класс передается в макрос IMPLEMENT_APP, в котором основное – это создание привычных функции main или WinMain и выполнение её.

Проверка работоспособности интерфейса: программа должна запуститься, а на экране выглядеть так:

Так же кнопка Load добавляет пустую строку, а UpLoad можно проверить в debug.

Дальше добавление всего остального функционала в программу:

А именно инициализация буферов в конструкторе:

, mDataEncrypt { 0 }
, mSizeEncrypt(0)
, mDataDecrypt{ 0 }
, mSizeDecrypt(0)

Добавление в конец конструктора CMainFrame инициализация JVM

try {
    mJNICloudLib.init();
} catch(const char* message) {
    std::cout << message << std::endl;
}

В методе Load очищаются все буферы и строки, если они не равны нулю

mItems.clear();
memset(mDataDecrypt, 0, mSizeDecrypt = SIZE_BUFFER);
memset(mDataEncrypt, 0, mSizeEncrypt = SIZE_BUFFER);
nRows = mGrid->GetNumberRows();
if(nRows != 0)
    mGrid->DeleteRows(0, mGrid->GetNumberRows());

и в обвертке исключения добавляются модульные функции.

Загружается файл из облака, сохраненный в облаке при проверки всех модулей.

mJNICloudLib.run("load", TOKEN, "test/keys_encrypt_upload.xml",
                 RESOURCES "/keys_encrypt_load.xml");

Чтение файла

CXMLFile::read(RESOURCES "/keys_encrypt_load.xml", mDataEncrypt, mSizeEncrypt);

Его дешифровка

CCipher::decrypt("1324", mDataEncrypt, mSizeEncrypt, mDataDecrypt, mSizeDecrypt);

И разбор xml-данных

CXMLFile::parse(mDataDecrypt, mSizeDecrypt, mItems);

Полученный вектор обходится в цикле и его элементы устанавливаются в таблицу

for(auto& item : mItems) {

    nRows = mGrid->GetNumberRows();
    mGrid->AppendRows();
    mGrid->SetCellValue(nRows, INDEX_LABEL_TITLE, item.title);
    mGrid->SetCellValue(nRows, INDEX_LABEL_LOGIN, item.login);
    mGrid->SetCellValue(nRows, INDEX_LABEL_PASSWORD, item.password);
    mGrid->SetCellValue(nRows, INDEX_LABEL_TYPE, item.type);
}

метод Fit вынесен за пределы цикла.

В итоге по нажатию кнопки «Load» таблица должна заполниться значениями:

В методе UpLoad так же очищаются все буферы

mItems.clear();
memset(mDataDecrypt, 0, mSizeDecrypt = SIZE_BUFFER);
memset(mDataEncrypt, 0, mSizeEncrypt = SIZE_BUFFER);

Затем обходится таблица и устанавливаются значения в вектор

nRows = mGrid->GetNumberRows();
for(int iRow = 0; iRow < nRows; iRow++) {

    SItem item;
    item.title = mGrid->GetCellValue(iRow, INDEX_LABEL_TITLE);
    item.login = mGrid->GetCellValue(iRow, INDEX_LABEL_LOGIN);
    item.password = mGrid->GetCellValue(iRow, INDEX_LABEL_PASSWORD);
    item.type = mGrid->GetCellValue(iRow, INDEX_LABEL_TYPE);
    mItems.emplace_back(item);
}

Дальше в обвертке исключения, первое – это происходит проверка, что вектор заполнен, чтобы не записать в облако пустой файл

if(mItems.empty() == true)
    throw "error: items is empty";

Далее собирается посылка из вектора в xml-формат

CXMLFile::collect(mItems, mDataDecrypt, mSizeDecrypt);

Данные шифруются

CCipher::encrypt("1324", mDataDecrypt, mSizeDecrypt, mDataEncrypt, mSizeEncrypt);

Записываются в файл

CXMLFile::write(RESOURCES "/keys_encrypt_upload.xml", mDataEncrypt, mSizeEncrypt);

И сохраняются в облако

mJNICloudLib.run("upload", TOKEN, "test", RESOURCES "/keys_encrypt_upload.xml");

Чтобы убедиться, что всё работает необходимо изменить данные в таблице, затем сохранить в облако, в консоли должно появиться «upload: success»

Android-приложение

Первое – это создается пустой проект в Android Studio:

Далее нажимается кнопка «Next», и переход к следующей вкладке:

  • В поле «Name» введено «Password Manager». Оно используется в 4 файлах:

Первые два файла – это настройки IDE для конкретного проекта.

Файл «strings.xml» – это файл ресурсов, где наименование присваивается переменной «app_name», которая в свою очередь используется в только в одном файле:

В файле «AndroidManifest.xml» — это в манифесте программы указывается имя приложения, которое будет отображаться под ярлыком программы на рабочем столе.

Дальше файл «settings.gradle.kts» в котором наименование присваивается переменной «rootProject.name». Эта переменная используется внутри Gradle для идентификации проекта в контексте многопроектных сборок и может быть использовано для ссылки на проект в других частях конфигурации Gradle.

  • В поле «Package name» введено «ru.rbkdev.passwordmanager». Это наименование встречается тоже в 4 файлах:

В файле «build.gradle.kts» наименование присваивается переменной «namespace» — это уникальное пространство приложения. Это то по какому пути компилятор будет находить файлы программы:

Это наименование непосредственно связано тремя другими файлами программы, 2 теста и главное «activity». В них через «package» подключается необходимое пространство. Во всех этих местах должны быть одинаковые наименования.

Последнее наименование присваивается переменной «applicationId» — это уникальный идентификатор, который будет использоваться в «Google Play Store», а также на устройстве. Например, если в проводнике телефона/эмулятора зайти в data/data, то можно увидеть установленные приложение с такими же уникальными идентификаторами, внутри которых, например сохраняется кэш, или можно сохранять какие-либо данные:

Этот уникальный идентификатор обычно составляется из доменов. Домен верхнего уровня (com, ru), нижнего уровня (rbkdev), наименование приложения (passwordmanager).

  • Далее в поле «Save location» введен путь «C:\PasswordManager\kotlin-android-app-password-manager» — это только путь, где будет лежать проект. В самих файлах проекта путь нигде не прописывается.

  • В следующих полях выбирается язык программирования Kotlin и минимальный SDK.

  • В последнем поле «Build configuration language» выбирается «Kotlin DSL» — это значит проектный файл gradle будет писаться на языке программирования «kotlin».

После нажатия «Finish» будет построен проект. Иногда сборка проекта по умолчанию не происходит. Решается изменением номера SDK

в файле «build.gradle.kts (Module:app)», который располагается в

Дальше первое, что необходимо сделать – это подключить вспомогательные библиотеки работы с облаком в этом файле «build.gradle.kts (Module:app)». В секцию «dependencies» добавлено:

build.gradle.kts

implementation(fileTree(
    mapOf(
        "dir" to "C:/PasswordManager/resources/library",
        "include" to listOf(
            "cloud-lib.jar",
            "commons-logging-1.2.jar",
            "httpclient-4.5.13.jar",
            "httpcore-4.4.16.jar",
            "xmlpull-1.1.3.4a.jar"
        )
    )))

Так же в секцию android необходимо добавить включение META-INF

android {

    …

    packaging {
        resources {
            excludes += "META-INF/DEPENDENCIES"
        }
    }
}

Дальше необходимо добавить соль (salt), вектор инициализации (iv) и токен (token). То есть указанные файлы при сборке из папки ресурсов будут копироваться в проект, так сборка файлов происходит только тех, которые находятся в проекте.

Для этого в gradle добавлена задача copy после секции «dependencies».

tasks.register("copy") {
    copy {
        from("C:/PasswordManager/resources")
        into(
          "C:/PasswordManager/kotlin-android-app-password-manager/app/src/main/res/raw")
        include("salt", "iv", "token")
    }
}

Здесь регистрируется новая задача, в которой копируются необходимый файлы из папки по пути from в папку по пути into, включая файлы include

Эту задачу можно свободно вызвать из консоли командой

./gradlew copy

и убедиться, что файлы правильно копируются в папку raw, где в Android проекте лежат бинарные файлы.

Так же папку raw можно создать через Android Studio. Для этого в Android Studio правой кнопкой вызывается контекстное меню по нажатию на папку с ресурсами «res». Далее «New» → «Android Resource Directory»

В появившемся окне «New Resource Directory» в поле «Resource type» выбрано «Raw» и нажата кнопка «OK» для завершения.

По завершению появится в ресурсах появится новая папка «raw».

Дальше, чтобы файлы копировались каждый раз при сборке проекта, необходимо добавить задачу перед сборкой:

tasks.preBuild {
    dependsOn("copy")
}

После этого в окне сборки появится задача «copy»:

Дальше в файл манифест AndroidManifest.xml добавить разрешение на использование интернета:

AndroidManifest.xml

<uses-permission android:name="android.permission.INTERNET"/>

Так же, так как используется устаревшая библиотеку Apache HTTP Legacy library необходимо внутри <application> указать тег

<uses-library android:name="org.apache.http.legacy" android:required="false"/>

Дальше, чтобы удостовериться, что всё подключено необходимо импортировать CloudLib в MainActivity:

MainActivity.h

import CloudLib

и вызвать загрузку файла из облака. И первый вопрос, куда файл сохранится. Сейчас android разрешает сохранять данные по пути «/data/user/0/ru.rbnz.passwordmanager/files», который показывается при вызове «baseContext.filesDir.absolutePath»

Этот путь и будет использоваться. Дальше необходимо подключить токен из файла. Для этого происходит обращение к ресурсам:

val token: String = resources.openRawResource(R.raw.token).readBytes()
  .toString(Charsets.UTF_8)

Здесь ресурс возвращается во входном стриме InputStream, происходит его чтение readBytes и перевод в String в кодировке UTF-8

Дальше сам вызов:

CloudLib.load(token, "test/keys_encrypt_upload.xml",
              "${baseContext.filesDir.absolutePath}/keys.xml")

Загруженный файл:

Код целиком
package ru.rbkdev.passwordmanager

import android.os.Bundle

import androidx.appcompat.app.AppCompatActivity

import CloudLib

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val token: String = resources.openRawResource(R.raw.token)
            .readBytes().toString(Charsets.UTF_8)

        CloudLib.load(token, "test/keys_encrypt_upload.xml",
                      "${baseContext.filesDir.absolutePath}/keys.xml")
    }
}

Модуль шифрования

Так же, как и в С++, будут использоваться данные вектора инициализации и соль, которые были скопированы на предыдущем этапе.

Дальше создается статический объект CCipher. В него будет передаваться класс ресурсов, для работы с «iv» и «salt»

cipher.kt

class CCipher(resources: Resources) {

В этом классе будет один метод process. А также переменные вектора инициализации и соли, которые будут создаваться один раз в начале программы и в них будут загружаться массивы байт из ресурсов, а также жить на протяжении всей программы.

private var mIv: ByteArray = resources.openRawResource(R.raw.iv).readBytes()
private var mSalt: ByteArray = resources.openRawResource(R.raw.salt).readBytes()

В сам метод «process» передаются: пароль, входной массив данных и тип процесса: шифрование (Cipher.ENCRYPT_MODE) или дешифрование (Cipher.DECRYPT_MODE). Так же возвращается выходной массив данных.

fun process(password: CharArray?, inText: ByteArray?, typeCipher: Int)
: ByteArray? {

Дальше так же, как и C++, необходимо воссоздать ключ по клиентскому паролю, соли и количества итерация. Для этого первое – это получение спецификации ключа по вышеизложенным параметрам:

val pbKeySpec: PBEKeySpec = PBEKeySpec(password, mSalt, 1324, 256)

Функция PBEKeySpec создает спецификацию ключа для алгоритмов PBE (Password-Based Encryption), в данном случае для алгоритма PBKDF2 (Password-Based Key Derivation Function 2)

Здесь и далее спецификация имеется ввиду просто параметры, которые объединены в объект. Если посмотреть в «debag», то можно увидеть все исходные параметры:

Дальше необходимо получить объект SecretKeyFactory, с помощью которого создается ключ.

val secretKeyFactory: SecretKeyFactory = 
  SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512")

Это функция вызывается с параметрами для алгоритма PBKDF2 с HMAC-SHA512.

Дальше генерация ключа с помощью объекта SecretKeyFactory и спецификации ключа

val keyBytes: ByteArray = secretKeyFactory.generateSecret(pbKeySpec).encoded

Дальше необходимо создать спецификацию ключа для алгоритма AES, по полученному ключу

val keySpec: SecretKeySpec = SecretKeySpec(keyBytes, "AES")

И так же спецификация для вектора инициализации

val ivSpec: IvParameterSpec = IvParameterSpec(mIv)

И в конце само шифрование. Получение объекта шифрования с алгоритмом AES в режиме CBC с PKCS5Padding

val cipher: Cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")

Дальше шифрование с параметрами типа процесса, спецификацией ключа и спецификацией вектора инициализации

cipher.init(typeCipher, keySpec, ivSpec)

И завершение шифрования, куда передается входной текст, а на выходе выходной текст

outText = cipher.doFinal(inText)

В конце метода всё это обрамлено в исключение и возвращает выходной массив

fun process(password: CharArray?, inText: ByteArray?, typeCipher: Int)
    : ByteArray? {

    var outText: ByteArray? = null

    try {

        val pbKeySpec: PBEKeySpec = PBEKeySpec(password, mSalt, 1324, 256)
        val secretKeyFactory: SecretKeyFactory =
            SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512")
        val keyBytes: ByteArray = 
            secretKeyFactory.generateSecret(pbKeySpec).encoded

        val keySpec: SecretKeySpec = SecretKeySpec(keyBytes, "AES")
        val ivSpec: IvParameterSpec = IvParameterSpec(mIv)

        val cipher: Cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
        cipher.init(typeCipher, keySpec, ivSpec)
        outText = cipher.doFinal(inText)

    } catch (exception: Exception) {
        exception.printStackTrace()
    }

    return outText
}

Дальше проверка дешифрования. Для этого необходимо загрузить файл с диска телефона и отправить в функцию дешифрования:

val cipher: CCipher = CCipher(resources)

CloudLib.load("<token>", "test/keys_encrypt.txt",
              "${baseContext.filesDir.absolutePath}/keys.xml")

val file: File = File("${baseContext.filesDir.absolutePath}/keys.xml")
if(file.exists()) {

    val encrypt = file.inputStream().readBytes()
    val decrypt: ByteArray? =
        cipher.process("0011".toCharArray(), encrypt, Cipher.DECRYPT_MODE)
    if(decrypt != null) {
        val result: String = decrypt.decodeToString()
        Log.d("TAG", result)
    }
}
Код целиком
package ru.rbkdev.passwordmanager

import android.os.Bundle

import androidx.appcompat.app.AppCompatActivity

import java.io.File
import javax.crypto.Cipher
import android.util.Log

import CloudLib

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val token = resources.openRawResource(R.raw.token).readBytes()
            .toString(Charsets.UTF_8)

        CloudLib.load(
            token,
            "test/keys_encrypt_upload.xml",
            "${baseContext.filesDir.absolutePath}/keys.xml"
        )

        val cipher: CCipher = CCipher(resources)

        val file: File =
            File("${baseContext.filesDir.absolutePath}/keys.xml")
        if (file.exists()) {

            val encrypt = file.inputStream().readBytes()
            val decrypt: ByteArray? = cipher.process(
                "0011".toCharArray(), encrypt, Cipher.DECRYPT_MODE)
            if(decrypt != null) {
                val result: String = decrypt.decodeToString()
                Log.d("TAG", "TEST DECRYPT")
                Log.d("TAG", result)
            }
        }
    }
}

И в логах будет исходный файл

Модуль работы с XML

Так же, как и в C++, создается структура данных, которая будет типом динамического массива:

xmlfile.kt

data class DItem (
    val type: String,
    val title: String,
    val login: String,
    val password: String
)

Дальше создается класс CXMLFile. В нем так же 4 метода read, write, parse и collect.

Функция «read» принимает на вход путь к файлу, а на выходе возвращает массив байт

fun read(path: String): ByteArray? {

    var result: ByteArray? = null

    val file: File = File(path)
    if(file.exists())
        result = file.inputStream().readBytes()

    return result
}

Функция «write» принимает на вход путь к файлу и массив данных, перезаписывает их и возвращает «true» в случае успеха

fun write(path: String, data: ByteArray?): Boolean {

    val file: File = File(path)
    if(file.exists() == false) {

        try {
            file.createNewFile()
        } catch (e: IOException) {
            e.printStackTrace()
            return false
        }
    }

    file.outputStream().write(data)
    return true
}

Дальше необходимо разобрать посылку в методе «parse». Туда передается входной массив данных на выходе список «DItem»

fun parse(inData: ByteArray) : List<DItem> {

Внутри создается сама переменная списка, которая будет наполняться

val outData: MutableList<DItem> = mutableListOf()

Дальше создание XML объектов для работы с массивом данных

val factory: DocumentBuilderFactory = DocumentBuilderFactory.newInstance()
val builder: DocumentBuilder = factory.newDocumentBuilder()
val doc: Document = builder.parse(inData.inputStream())

Дальше сам обход. Получен «root» элемент, получен список его детей и размер списка и произведен обход этого списка в цикле

val root: Element = doc.documentElement
val listRoot: NodeList = root.childNodes
val sizeRoot: Int = listRoot.length
for(i in 0 until sizeRoot) {

Внутри цикла необходимо проверять узел, что он является типом Node.ELEMENT_NODE

val nodeBlock: Node = listRoot.item(i)
if(nodeBlock.nodeType == Node.ELEMENT_NODE) {

Тогда узел можно преобразовать к типу «Element»

val block: Element = nodeBlock as Element

Дальше вытаскиваются атрибуты и сохраняются в выходном списке

outData.add(DItem(
    "block",
    block.getAttribute("title"),
    block.getAttribute("login"),
    block.getAttribute("password")
))

И в конце тоже самое проделывается для элементов «item»

val listBlock: NodeList = block.childNodes
val sizeBlock: Int = listBlock.length
for(j in 0 until sizeBlock) {

    val nodeItem = listBlock.item(j)
    if(nodeItem.nodeType == Node.ELEMENT_NODE) {

        val item = nodeItem as Element
        outData.add(DItem(
            "item",
            item.getAttribute("title"),
            item.getAttribute("login"),
            item.getAttribute("password")
        ))
    }
}

В конце возвращается список

Проверка:

val list: List<DItem> = CXMLFile.parse(decrypt)
Log.d("TAG", "TEST PARSE")
list.forEach {
    Log.d("TAG", "${it.type} ${it.title} ${it.login} ${it.password}")
}
Код целиком
package ru.rbkdev.passwordmanager

import android.os.Bundle

import androidx.appcompat.app.AppCompatActivity

import java.io.File
import javax.crypto.Cipher
import android.util.Log

import CloudLib

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val token = resources.openRawResource(R.raw.token).readBytes()
            .toString(Charsets.UTF_8)

        CloudLib.load(
            token,
            "test/keys_encrypt_upload.xml",
            "${baseContext.filesDir.absolutePath}/keys.xml"
        )

        val cipher: CCipher = CCipher(resources)

        val file: File =
            File("${baseContext.filesDir.absolutePath}/keys.xml")
        if (file.exists()) {

            val encrypt = file.inputStream().readBytes()
            val decrypt: ByteArray? = cipher.process(
                "0011".toCharArray(), encrypt, Cipher.DECRYPT_MODE)
            if(decrypt != null) {
                val list: List<DItem> = CXMLFile.parse(decrypt)
                Log.d("TAG", "TEST PARSE")
                list.forEach {
                    Log.d("TAG",
                    "${it.type} ${it.title} ${it.login} ${it.password}")
                }
            }
        }
    }
}

Должен появиться необходимый список

Дальше собирается посылка в методе «collect». В неё передается список элементов, а на выходе получается массив данных

fun collect(inData: List<DItem>): ByteArray {

Внутри такая же логика обхода списка, как и в C++.

Первое создаются объекты для работы с XML

val factory: DocumentBuilderFactory = DocumentBuilderFactory.newInstance()
val builder: DocumentBuilder = factory.newDocumentBuilder()
val doc: Document = builder.newDocument()

Дальше создаются рабочие переменные

val root = doc.createElement("root")
var block: Element? = null
var item: Element? = null

Затем в цикле обходится список и разбирается по типу. Если элемент соответствует необходимому типу, то параметры элемента списка сохраняются в XML-элемент, а XML-элемент сохраняется в родительский элемент

inData.forEach { it ->

    if(it.type == "block") {
        block = doc.createElement(it.type)
        block?.let { block ->
            block.setAttribute("title", it.title)
            block.setAttribute("login", it.login)
            block.setAttribute("password", it.password)
            root.appendChild(block)
        }
    }

    if(block != null && it.type == "item") {
        item = doc.createElement(it.type)
        item?.let {item ->
            item.setAttribute("title", it.title)
            item.setAttribute("login", it.login)
            item.setAttribute("password", it.password)
            block?.appendChild(item)
        }
    }
}
doc.appendChild(root)

Дальше необходимо преобразовать полученный «Document» в массив байт. Для этого создаются выходные стримы

val outStream: ByteArrayOutputStream = ByteArrayOutputStream()
val result: StreamResult = StreamResult(outStream)

и вспомогательные объекты трансформации, куда передается полученный «Document»

val transformerFactory: TransformerFactory = TransformerFactory.newInstance()
val transformer: Transformer = transformerFactory.newTransformer()
val source: DOMSource = DOMSource(doc)

Дальше сама трансформации с включением отступов в конечном XML-формате

transformer.setOutputProperty(OutputKeys.INDENT, "yes")
transformer.transform(source, result)

И возврат выходного массива

return outStream.toByteArray()

Проверка:

val array: ByteArray = CXMLFile.collect(list)

val result: String = array.decodeToString()

Log.d("TAG", "TEST COLLECT")
Log.d("TAG", result)
Код полностью
package ru.rbkdev.passwordmanager

import android.os.Bundle

import androidx.appcompat.app.AppCompatActivity

import java.io.File
import javax.crypto.Cipher
import android.util.Log

import CloudLib

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val token = resources.openRawResource(R.raw.token).readBytes()
            .toString(Charsets.UTF_8)

        CloudLib.load(
            token,
            "test/keys_encrypt_upload.xml",
            "${baseContext.filesDir.absolutePath}/keys.xml"
        )

        val cipher: CCipher = CCipher(resources)

        val file: File =
            File("${baseContext.filesDir.absolutePath}/keys.xml")
        if (file.exists()) {

            val encrypt = file.inputStream().readBytes()
            val decrypt: ByteArray? = cipher.process(
                "0011".toCharArray(), encrypt, Cipher.DECRYPT_MODE)
            if(decrypt != null) {

                val list: List<DItem> = CXMLFile.parse(decrypt)

                val array: ByteArray = CXMLFile.collect(list)

                val result: String = array.decodeToString()

                Log.d("TAG", "TEST COLLECT")
                Log.d("TAG", result)
            }
        }
    }
}

На выходе правильный XML-формат:

Интерфейс

Последнее, что необходимо сделать – это интерфейс

Интерфейс будет такой же простой. Сверху две кнопки «Load» и «Upload», а вместо таблицы создан ряд из «EditText» в «TableLayout».

Итого макет в файле «activity_main.xml» будет следующим:

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout

    xmlns:android="http://schemas.android.com/apk/res/android"

    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <Button
            android:id="@+id/btnLoad"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Load"/>

        <Button
            android:id="@+id/btnUpload"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Upload"/>

    </LinearLayout>

    <TableLayout
        android:id="@+id/tableLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</LinearLayout>

Здесь общий «LinearLayout» в нем «LinearLayout» с двумя кнопками одинаково занимающие горизонтально пространство и сам «TableLayout», у которого строчки будут «TableRow», а в строках будет 3 «EditText»

Дальше MainActivity.kt. По умолчанию IDE создает класс MainActivity.

MainActivity.kt

class MainActivity : AppCompatActivity() {

В этом классе прописываются глобальные переменные класса: объект работы с шифрованием, токен, локальный путь, пароль и переменная выполнения результата (Для простоты в неё записывается «success» или «error»)

Так же в «companion object» прописываются константы – номера колонок в таблице

private var mCipher: CCipher? = null

private var mToken: String = ""
private var mPathLocal: String = ""
private var mPassword: CharArray = charArrayOf()
private var mResult: String = ""

companion object {

    const val INDEX_LABEL_TITLE: Int = 0
    const val INDEX_LABEL_LOGIN: Int =  1
    const val INDEX_LABEL_PASSWORD: Int =  2
    const val INDEX_LABEL_TYPE: Int =  3
}

Дальше в классе MainActivity при создании проекта написана функция onCreate и добавлен макет activity_main

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

В этом методе прописывается весь функционал

Происходит инициализация переменных: создается объект CCipher  в котором загружаются массивы «salt» и «iv», присваиваются значения токену, паролю и локальному пути

mCipher = CCipher(resources)

mToken = resources.openRawResource(R.raw.token).readBytes().toString(Charsets.UTF_8)
mPassword = charArrayOf( '0', '0', '1', '1')
mPathLocal = "${baseContext.filesDir.absolutePath}/keys_encrypt_upload.xml"

Дальше находятся элементы интерфейса на макете:

val btnLoad: Button = findViewById(R.id.btnLoad)
val btnUpload: Button = findViewById(R.id.btnUpload)
val tableLayout: TableLayout = findViewById(R.id.tableLayout)

Дальше на кнопки устанавливается слушатель с помощью функции setOnClickListener

btnLoad.setOnClickListener {
btnUpload.setOnClickListener {

Для кнопки «Load» слушатель будет состоять из следующих этапов: из облака будет загружаться зашифрованный файл, его загрузка в память, расшифровка и заполнение таблицы:

mResult = "error"

if(CloudLib.load(mToken, "test/keys_encrypt_upload.xml", mPathLocal) == true) {

    val dataEncrypt: ByteArray? = CXMLFile.read(mPathLocal)
    if(dataEncrypt != null) {

        mCipher?.let { cipher ->

            val dataDecrypt: ByteArray? = cipher.process(mPassword, dataEncrypt, 
                                                         Cipher.DECRYPT_MODE)
            if(dataDecrypt != null) {

                val list: List<DItem> = CXMLFile.parse(dataDecrypt)
                if(list.isEmpty() == false) {

                    tableLayout.removeAllViews()

                    list.forEach {

                        val tableRow = TableRow(this)

                        val title = EditText(this)
                        title.gravity = Gravity.CENTER
                        title.setText(it.title)
                        tableRow.addView(title)

                        val login = EditText(this)
                        login.gravity = Gravity.CENTER
                        login.setText(it.login)
                        tableRow.addView(login)

                        val password = EditText(this)
                        password.gravity = Gravity.CENTER
                        password.setText(it.password)
                        tableRow.addView(password)

                        val type = EditText(this)
                        type.gravity = Gravity.CENTER
                        type.setText(it.type)
                        tableRow.addView(type)

                        tableLayout.addView(tableRow)

                        mResult = "success"
                    }
                }
            }
        }
    }
}

Toast.makeText(baseContext, mResult, Toast.LENGTH_SHORT).show()

На что обратить внимание – это заполнение таблицы происходит путем создания новых элементов «EditText», которые сначала каждый устанавливается в «TableRow», затем «TableRow» устанавливается «TableLayout». В конце с помощью «Toast» выдаем результат: ошибка или успех.

Для кнопки «Upload» слушатель будет состоять из следующих этапов:

Обход таблицы и сохранение значение в списке элементов, создание XML-массива данных, его шифрование, запись на диск, загрузка в облако:

mResult = "error"

val list: MutableList<DItem> = mutableListOf()

val rowCount: Int = tableLayout.childCount
for(iRow in 0 until rowCount) {

    val rowView: View = tableLayout.getChildAt(iRow)
    if(rowView is TableRow) {

        val item = DItem("", "", "", "")

        val colCount = rowView.childCount
        for(iCol in 0 until colCount) {

            val itemView = rowView.getChildAt(iCol)
            if(itemView is EditText) {

                when (iCol) {
                    INDEX_LABEL_TITLE -> item.title = itemView.text.toString()
                    INDEX_LABEL_LOGIN -> item.login = itemView.text.toString()
                    INDEX_LABEL_PASSWORD -> item.password = itemView.text.toString()
                    INDEX_LABEL_TYPE -> item.type = itemView.text.toString()
                }
            }
        }

        list.add(item)
    }
}

val dataDecrypt: ByteArray = CXMLFile.collect(list)
if(dataDecrypt.isNotEmpty()) {

    mCipher?.let { cipher ->

        val dataEncrypt: ByteArray? = cipher.process(mPassword, dataDecrypt,
                                                     Cipher.ENCRYPT_MODE)
        if(dataEncrypt != null) {

            CXMLFile.write(mPathLocal, dataEncrypt)

            if(CloudLib.upload(mToken, "test", mPathLocal) == true) {
                mResult = "success"
            }
        }
    }
}

Toast.makeText(baseContext, mResult, Toast.LENGTH_SHORT).show()

Здесь обход таблицы происходит с учетом индексов колонки. А в конце так же отображается результат

Проверка:

При нажатии кнопки «Load» должна загрузить таблица, а внизу экрана появиться «success»

По нажатию кнопки «Upload» сохраняются значения в облаке и появляется «success»

Чтобы убедиться в полной работе системы необходимо изменить значение на одном из устройстве, загрузить в облако, а на втором устройстве сохранить из облако и убедиться, что пришло новое значение

Результат

Была проведена работа по создания небольшого приложения для хранения паролей. Мы убедились в том, что значения шифруются и хранятся в облаке. Так же убедились в том, что данные никак не сохраняются на жестком диске и работа с ними происходит в оперативной памяти. Убедились, что данные легко загружаются как на одно устройство, так и на второе. Это минимальный пример, который показывает вектор разработки приложения с паролями, связывающих ПК ⇆ облако ⇆ телефон в одну сеть. Этот фундамент можно использовать не только для хранения паролей, но и сделать небольшую экосистему с учетом того, что скорость передачи данных и скорость обработки данных увеличивается со временем. То есть мощности с серверной части перекинуть на клиента, а сам условный сервер использовать только для хранения данных.

Комментарии (35)


  1. PikPok77
    15.06.2024 08:19

    Хорошая работа. Но как с монетозацией?


    1. rbkdev Автор
      15.06.2024 08:19
      +3

      Нет монетизации. Надеюсь информация дороже


    1. Ponchik666
      15.06.2024 08:19

      0 десятых ;)


  1. gudvinr
    15.06.2024 08:19
    +13

    Как будто вы свой курсач/диплом скопировали и вставили.
    Манера подачи очень шаблонная и используемые средства довольно сомнительные, учитывая выбор языка. Как будто за использование JNI и статью на заборе дают дополнительные баллы в зачёт


    1. rbkdev Автор
      15.06.2024 08:19

      Придерживался официального стиля с вкраплениями вольностей, так как посчитал, что это самый лучший вариант для мануала. JNI выбран как один из инструментов работы с облаком, так как компания предоставляет библиотеку на java, в которой происходит обмен данными. Поэтому выбрал такой вариант, чтобы не описывать взаимодействие по REST, и чтобы одну и туже библиотеку подключить на разных платформах.


  1. exmachine
    15.06.2024 08:19
    +14

    Десктопные кроссплатформенные парольные менеджеры которые хранят БД в файле:
    * https://www.keepassx.org
    * https://keepass.info
    * https://keepassxc.org

    Для мобилки тиких приложений больше.

    Все работают поверх одного формата kdbx

    Файл с паролями автоматически шифруется мастер паролем или локальным ключем.


    1. waytoroot
      15.06.2024 08:19
      +3

      keepass: существует

      автор: изобретает велосипед


    1. rbkdev Автор
      15.06.2024 08:19
      +12

      Хабр не образовательный, так и запишем


    1. 0x2E757
      15.06.2024 08:19
      +1

      Есть еще замечательный https://keeweb.info/, у которого есть в том числе web версия.


  1. dsoastro
    15.06.2024 08:19

    Я делаю так:

    #!/bin/bash

    if [ 1 -gt $# ]; then
    echo 'Usage: note_enc "text to encrypt" > your_file '
    exit
    fi

    echo $1 | gpg --symmetric --cipher-algo AES256

    Синхронизация между устройствами через git


    1. Prototik
      15.06.2024 08:19
      +6

      Специально для Вас уже давно сделали pass, ровно через gpg ключи и засовыванием этого в git репу.


  1. schwagerina
    15.06.2024 08:19

    Спасибо большое за проделанную работу, попробую повторить!


    1. voldemar_d
      15.06.2024 08:19
      +2

      nozacem.lv

      (извините, не удержался)


  1. IvanProvinciaTver
    15.06.2024 08:19
    +2

    Получилось
    Крайне
    Полезное
    Ни**я
    (с) Доктор Дью

    Автор,
    1. научитесь структурировать информацию, оборачивать ваши мысли в абзацы, параграфы и главы, предоставлять план действий, мыслить проектно.
    2. посмотрите кино Игра в имитацию, где вам доступно объяснят:
    знание, о том, что содержимое представляет из себя XML - снижает криптостойкость.
    Вы хоть обмажтесь аесами и гостами,
    3. хранить полный файл в памяти - это залог успеха!


    1. gudvinr
      15.06.2024 08:19

      знание, о том, что содержимое представляет из себя XML - снижает криптостойкость

      Без конкретных данных это просто демагогия. Допустим, иметь общее понимание полезно, но если это практического влияния иметь не будет, то от знания толка никакого нет

      хранить полный файл в памяти - это залог успеха!

      Если это файл, который программа сама генерирует и/или есть ограничения на размер буфера для чтения, то никакой проблемы тут нет - распакованный XML с паролями - это не архив фоток


      1. Le0Wolf
        15.06.2024 08:19

        Проблема есть с тем, что память процесса может прочитать кто угодно и тогда у злоумышленника будут вообще все пароли, причем в открытом виде


        1. gudvinr
          15.06.2024 08:19
          +1

          Во-первых, это зависит от OS. Не везде можно свободно читать память другого процесса (ну, может быть для модуля ядра - везде).

          Во-вторых, так или иначе пароли в любом случае должны в какой-то момент попасть в память, а если и не сами пароли, то мастер-ключ для расшифровки. А защита от этого уже будет сильно разниться для разных ОС и разных процессоров. На ARM есть TrustZone, на x86 -TPM/SGX.

          Вы сами говорите, что автор плоховато подаёт информацию, если сюда ещё воткнуть вопросы кросс платформенной защиты памяти - можно на пару таких же томов настрочить. Но если это вас возмущает, можете сами тоже написать, как правильно надо, а не вот это баловство. Заодно и покажете, насколько ужасно обмазывать XML аесом


          1. Le0Wolf
            15.06.2024 08:19

            Меня это не возмущает) И вообще изначальный комментарий оставил совсем не я. Я лишь предположил проблемы созданного решения.

            Но в целом, информация действительно подана не очень удобно. Возможно, было бы лучше написать целый цикл статей, причем в первой - анализ рынка и особенности той или иной выбраннной технологии

            Далее, хранение паролей - это про криптографию и безопасность. Но судя по всему, автор либо специально сильно упростил этот момент (статья в этом плане скорее о том, как делать не надо), либо просто не очень разбирается в теме (что уже плохо). Из того, что плохо: 1) данные в памяти не зашифрованы, 2) хранятся все сразу и под одним ключом, 3) отображаются тоже все сразу, а не по очереди, 4) данные для шифрования/расшифрования хранятся в самом приложении,как его части, хотя абсолютно во всех адекватных реализациях мастер пароль вообще ни где не хранится, а соль всегда будет разная, для каждой записи

            Даже если оставить криптографию, то менеджер паролей, по хорошему, должен не просто быть нативным приложением для выбранной операционной системы, но и нативно в нее интегрироваться, как собственно, менеджер паролей. То есть, это подразумевает плагины для браузеров, поддержку автозаполнения на том же андроид, например и т.д.

            И в заключение: статья в техническом плане в принципе достаточно слабая. По факту, тут нет ни разбора принципа выбора технологий (если статья позиционируется как для новичков), ни хардкорных алгоритмов и подходов к надёжной защите паролей (если статья на более опытную аудиторию). Ну и в принципе, статья больше не про гранение паролей, а о том, как скрестить ужа с удавом, как дебажить и т.д. и если уж захотелось интеграции с о лаком, то лучше бы стоило реализовать систему плагинов и как пример такого плагина - написать поддержку одного конкретного облака, причем не по принципу "я нашел всего одну библиотеку интеграции, но она на другом языке программирования, поэтому попытаемся все скрестить", а "раз пошло такое дело, давайте разберемся, как оно там работает и перепишем только нужный нам функционал уже на нашем языке"


        1. MixNN
          15.06.2024 08:19

          Давайте я Вам помогу. Проблема в том что

          • Экран на который выведен пароль может читать кто угодно

          • Если пароль озвучивать голосом - его может послушать кто угодно

          • Если пароль печатать на принтере без вывода на экран - его может посмотреть кто угодно

          Как вариант решения не дешифровывать пароли. Пусть всегда остаются зашифрованными.


    1. FreeNickname
      15.06.2024 08:19
      +2

      посмотрите кино Игра в имитацию, где вам доступно объяснят: знание, о том, что содержимое представляет из себя XML - снижает криптостойкость.

      По другим 2 пунктам претензий нет, но этот применим только если пользоваться криптографией 1940-х годов. Мы, к счастью, преодолели этот этап.


  1. snark87
    15.06.2024 08:19

    Простите, несколько вопросов по используемой криптографии.

    1) Я так понял вы один раз генерируете соль и IV, линкуете и они переиспользуются при каждом шифровании? Если это правда, то так делать нельзя. Например, вот почему: https://derekwill.com/2021/01/01/aes-cbc-mode-chosen-plaintext-attack/

    2) А почему AES CBC, собственно? Ведь в нем же нет аутентификации. https://alicegg.tech/2019/06/23/aes-cbc


  1. LaRN
    15.06.2024 08:19
    +1

    Может имеет смысл шифровать не весь файл, а только логин и пароль для каждого сервиса. Тогда полностью расшифрованный файл не будет в памяти лежать. А расшифровывать реквизиты только перед входом в аккаунт для конкретного сервиса.


  1. iliar
    15.06.2024 08:19
    +3

    Велосипед, причём довольно кривой. С первого взгляда вижу сразу проблемы:

    1) Привязка к яндекс.диску. Если уж параноить то пароли нужно держать на собственном сервере, а не в публичных облаках.

    2) Для записи есть только поля логин/пароль. Часто этого мало. Одноразовые пароли. Временные пароли. Дополнительные данные аутентификации. И т. д.

    3) Нет объединения изменений. Скажем если между чтением файла и записью, файл был изменён на другом устройстве. То по хорошему перед записью нужно проверить менялся ли файл и если менялся, то нужно объединить изменения.


    1. vvzvlad
      15.06.2024 08:19

      Если уж параноить то пароли нужно держать на собственном сервере, а не в публичных облаках.

      А если уж есть свой сервер, можно использовать bitwarden.


  1. nyarovoy
    15.06.2024 08:19
    +1

    Тоже сейчас копаюсь и пишу свой менеджер паролей. Статью пробежал лишь мельком, так что если про мои замечания было что-то написано, прошу не сильно ругать.

    Для хранения паролей будет использоваться файл формата xml, который будет шифроваться методом AES-256 и храниться в облаке.

    По итогу придется вытаскивать все пароли в оперативку и держать там. Не то, чтобы прям совсем критично, но из оперативки их тоже можно прочитать, насколько помню.

    Для работы с API-диска требуется логин и пароль, но вместо того, чтобы каждый раз вводить их, был перехвачен токен авторизации и использовался для работы с облаком. 

    Это первое - срок действия токена авторизации может закончиться и придется заново получать токен.

    Чтобы напрямую работать с массивом данными и не подгружать их каждый раз из файлов, эти массивы сразу импортируются в исполняемый файл. Для этого полученные файлы преобразовываются в объектные файлы, с помощью инструменты «objcopy» (инструмент поставляется вместе с MinGW), а потом линкуются в exe-файл.

    Это то, с чем я долго боролся и получилось побороть только с костылем. На винде так делать очень плохо. Если кто-то получит доступ к exe файлу, а это может сделать любой вирус, нацеленный специально на атаку вашего менеджера паролей, то ключ шифрования будет получить достаточно легко. И хранить ключи просто в файлах тоже плохо: к ним также есть доступ. Вы используете PBKDF2, так выберите надежный пароль и уже с его помощью генерируйте ключи. Правда, на мой взгляд, подход "один пароль, чтобы править всем" тоже слегка странный. Также, как правильно отметили выше, использовать один и тот же IV для AES не особо правильно и безопасно.

    На мой взгляд, наилучшим решением было бы написание своего сервера под хранение паролей со сквозным шифрованием. Причем каждый пароль шифровать своим ключом. В идеале, собрать систему "выстрели пользователю в колено", чтобы нельзя было идентифицировать, кому принадлежит пароль в базе, а получать конкретный пароль только после ввода названия сервиса и логина (хранить название сервиса и логин напрямую плохо, кстати). Да, это доставит неудобства пользователю, но в таком решении безопасность пароля максимальна, потому что:
    1) Без идентификации пользователя, сервиса и логина, даже если пароль утечет и его каким-то чудом правильно дешифруют, это будет просто пароль, который неизвестно куда применять
    2) Уникальные ключи шифрования дают возможность не переживать за то, что дешифрование одного пароля даст возможность узнать остальные пароли в базе
    3) Даже если злоумышленник получит доступ к устройству пользователя, в котором был произведен вход в систему, без знаний о сервисах и логинах для которых есть записи в базе.


  1. Chupaka
    15.06.2024 08:19
    +1

    В заголовке не хватает то ли запятой, то ли дефиса, то ли знания английского...


    1. FreeNickname
      15.06.2024 08:19

      Тоже резануло. Кривой пословный перевод password manager (ну и да, тогда надо с дефисом). А вообще, по-русски, конечно, менеджер паролей. Ну, условно по-русски :) Управитель ключей. Владыка тайн. Хранитель слов силы. Тайноведецъ. Что-то в этом духе :)


      1. Chupaka
        15.06.2024 08:19
        +2

        Ключница!


        1. aleksandy
          15.06.2024 08:19

          Она же водку делает.

          Хотя... генерирование паролей в изменённом сознании тоже, своего рода, криптостойкость.


        1. FreeNickname
          15.06.2024 08:19

          И впрямь)


        1. voldemar_d
          15.06.2024 08:19

          Мастер ключей (из Матрицы-перезагрузки ;)


  1. odisseylm
    15.06.2024 08:19

    У автора в школе наверное 5 (то есть 12) по сочинению была. Вместо 100 слов написать 1000, это талант. У меня с этим проблемы были ))
    Для курсовой, наверное, это неплохо, но для хабра не ок. Автор берегите своё и чужое время, не пишите ненужных буковок. Даже учебные мануалы не пишутся с расчётом на идиотов (если, конечно, это не гос организация).


  1. souls_arch
    15.06.2024 08:19

    Шел 2024 год... автор бы лучше больше внимания gui и документации в коде уделил, чем изобретать никому не нужный велосипед с 0 и так подробно излагать механику того, что сейчас в пару строк кода умещается. Когда подобный фуллстек вебапп/мобайлапп (да пес с ним, десктопный) можно разработать что на джава, что на крестах гораздо проще и быстрее. Сохранение в файл конечно интересная идея для безопасности и сохранения бабок/ресурсов (не надо ращворачивать или арендовывать сервак, но если для лички только, можно и локальный апп создать). Идеи хорошие, а вот реализация лично мне не очень зашла.


  1. Vlad2027
    15.06.2024 08:19

    По-моему это самый длинный пост, который мне приходилось листать) Можно было бы весь код просто в гитхаб скинуть и сократить длину поста раз в 5 что уже можно было бы хотя бы попытаться прочитать.


  1. BlithePeach
    15.06.2024 08:19

    Я прошу прощения конечно, но это случайно не дипломная работа какого-нибудь студента?