diff --git a/dist/generated/qrcode_data_pb.js b/dist/generated/qrcode_data_pb.js
index 5349e36e18de696a3d726d5a8e29603f5b780fc1..e903a4aea9805dde1a3e87e2cbbc287f411fef3c 100644
--- a/dist/generated/qrcode_data_pb.js
+++ b/dist/generated/qrcode_data_pb.js
@@ -980,6 +980,7 @@ $root.vereign = (function () {
                  * @property {string|null} [key] IpfsAttachmentData_V1 key
                  * @property {string|null} [contentHash] IpfsAttachmentData_V1 contentHash
                  * @property {string|null} [head] IpfsAttachmentData_V1 head
+                 * @property {string|null} [contentType] IpfsAttachmentData_V1 contentType
                  */
                 /**
                  * Constructs a new IpfsAttachmentData_V1.
@@ -1023,6 +1024,13 @@ $root.vereign = (function () {
                  * @instance
                  */
                 IpfsAttachmentData_V1.prototype.head = "";
+                /**
+                 * IpfsAttachmentData_V1 contentType.
+                 * @member {string} contentType
+                 * @memberof vereign.protobuf.qrcode_data.IpfsAttachmentData_V1
+                 * @instance
+                 */
+                IpfsAttachmentData_V1.prototype.contentType = "";
                 /**
                  * Creates a new IpfsAttachmentData_V1 instance using the specified properties.
                  * @function create
@@ -1054,6 +1062,8 @@ $root.vereign = (function () {
                         writer.uint32(/* id 3, wireType 2 =*/ 26).string(message.contentHash);
                     if (message.head != null && Object.hasOwnProperty.call(message, "head"))
                         writer.uint32(/* id 4, wireType 2 =*/ 34).string(message.head);
+                    if (message.contentType != null && Object.hasOwnProperty.call(message, "contentType"))
+                        writer.uint32(/* id 5, wireType 2 =*/ 42).string(message.contentType);
                     return writer;
                 };
                 /**
@@ -1098,6 +1108,9 @@ $root.vereign = (function () {
                             case 4:
                                 message.head = reader.string();
                                 break;
+                            case 5:
+                                message.contentType = reader.string();
+                                break;
                             default:
                                 reader.skipType(tag & 7);
                                 break;
@@ -1143,6 +1156,9 @@ $root.vereign = (function () {
                     if (message.head != null && message.hasOwnProperty("head"))
                         if (!$util.isString(message.head))
                             return "head: string expected";
+                    if (message.contentType != null && message.hasOwnProperty("contentType"))
+                        if (!$util.isString(message.contentType))
+                            return "contentType: string expected";
                     return null;
                 };
                 /**
@@ -1165,6 +1181,8 @@ $root.vereign = (function () {
                         message.contentHash = String(object.contentHash);
                     if (object.head != null)
                         message.head = String(object.head);
+                    if (object.contentType != null)
+                        message.contentType = String(object.contentType);
                     return message;
                 };
                 /**
@@ -1185,6 +1203,7 @@ $root.vereign = (function () {
                         object.key = "";
                         object.contentHash = "";
                         object.head = "";
+                        object.contentType = "";
                     }
                     if (message.cid != null && message.hasOwnProperty("cid"))
                         object.cid = message.cid;
@@ -1194,6 +1213,8 @@ $root.vereign = (function () {
                         object.contentHash = message.contentHash;
                     if (message.head != null && message.hasOwnProperty("head"))
                         object.head = message.head;
+                    if (message.contentType != null && message.hasOwnProperty("contentType"))
+                        object.contentType = message.contentType;
                     return object;
                 };
                 /**
@@ -1216,6 +1237,7 @@ $root.vereign = (function () {
                  * @property {vereign.protobuf.qrcode_data.IIpfsContentData_V1|null} [plaintText] IpfsData_V1 plaintText
                  * @property {vereign.protobuf.qrcode_data.IIpfsContentData_V1|null} [html] IpfsData_V1 html
                  * @property {Array.<vereign.protobuf.qrcode_data.IIpfsAttachmentData_V1>|null} [attachments] IpfsData_V1 attachments
+                 * @property {string|null} [version] IpfsData_V1 version
                  */
                 /**
                  * Constructs a new IpfsData_V1.
@@ -1253,6 +1275,13 @@ $root.vereign = (function () {
                  * @instance
                  */
                 IpfsData_V1.prototype.attachments = $util.emptyArray;
+                /**
+                 * IpfsData_V1 version.
+                 * @member {string} version
+                 * @memberof vereign.protobuf.qrcode_data.IpfsData_V1
+                 * @instance
+                 */
+                IpfsData_V1.prototype.version = "";
                 /**
                  * Creates a new IpfsData_V1 instance using the specified properties.
                  * @function create
@@ -1283,6 +1312,8 @@ $root.vereign = (function () {
                     if (message.attachments != null && message.attachments.length)
                         for (var i = 0; i < message.attachments.length; ++i)
                             $root.vereign.protobuf.qrcode_data.IpfsAttachmentData_V1.encode(message.attachments[i], writer.uint32(/* id 3, wireType 2 =*/ 26).fork()).ldelim();
+                    if (message.version != null && Object.hasOwnProperty.call(message, "version"))
+                        writer.uint32(/* id 4, wireType 2 =*/ 34).string(message.version);
                     return writer;
                 };
                 /**
@@ -1326,6 +1357,9 @@ $root.vereign = (function () {
                                     message.attachments = [];
                                 message.attachments.push($root.vereign.protobuf.qrcode_data.IpfsAttachmentData_V1.decode(reader, reader.uint32()));
                                 break;
+                            case 4:
+                                message.version = reader.string();
+                                break;
                             default:
                                 reader.skipType(tag & 7);
                                 break;
@@ -1378,6 +1412,9 @@ $root.vereign = (function () {
                                 return "attachments." + error;
                         }
                     }
+                    if (message.version != null && message.hasOwnProperty("version"))
+                        if (!$util.isString(message.version))
+                            return "version: string expected";
                     return null;
                 };
                 /**
@@ -1412,6 +1449,8 @@ $root.vereign = (function () {
                             message.attachments[i] = $root.vereign.protobuf.qrcode_data.IpfsAttachmentData_V1.fromObject(object.attachments[i]);
                         }
                     }
+                    if (object.version != null)
+                        message.version = String(object.version);
                     return message;
                 };
                 /**
@@ -1432,6 +1471,7 @@ $root.vereign = (function () {
                     if (options.defaults) {
                         object.plaintText = null;
                         object.html = null;
+                        object.version = "";
                     }
                     if (message.plaintText != null && message.hasOwnProperty("plaintText"))
                         object.plaintText = $root.vereign.protobuf.qrcode_data.IpfsContentData_V1.toObject(message.plaintText, options);
@@ -1442,6 +1482,8 @@ $root.vereign = (function () {
                         for (var j = 0; j < message.attachments.length; ++j)
                             object.attachments[j] = $root.vereign.protobuf.qrcode_data.IpfsAttachmentData_V1.toObject(message.attachments[j], options);
                     }
+                    if (message.version != null && message.hasOwnProperty("version"))
+                        object.version = message.version;
                     return object;
                 };
                 /**
diff --git a/dist/index.d.ts b/dist/index.d.ts
index 348a83c2e2ad5063b65da49d3bfba64274514e81..c116b57ea3b5d35efc783c395e9c6dcd01b755bd 100644
--- a/dist/index.d.ts
+++ b/dist/index.d.ts
@@ -5,6 +5,7 @@ export { default as VerificationService } from "./services/VerificationService";
 export { default as StatusesService } from "./services/StatusesService";
 export { default as CommonUtils } from "./utils/common";
 export { default as QrCodeDataService } from "./services/QrCodeDataService";
+export { default as IPFSService } from "./services/IPFSService";
 export { default as CryptoService } from "./services/CryptoService";
 export { default as VerificationError } from "./services/VerificationService/VerificationError";
 export { default as QrCodeTemplate } from "./utils/qrCodeTemplateUtils";
diff --git a/dist/index.js b/dist/index.js
index 66ed166f2afc3790d69cccaeb73e04bf618734fc..04de50511a1bf02c7a61238c090f246e9ddbce48 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -13,7 +13,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
     return (mod && mod.__esModule) ? mod : { "default": mod };
 };
 Object.defineProperty(exports, "__esModule", { value: true });
-exports.QrCodeTemplate = exports.VerificationError = exports.CryptoService = exports.QrCodeDataService = exports.CommonUtils = exports.StatusesService = exports.VerificationService = exports.CloudflareService = void 0;
+exports.QrCodeTemplate = exports.VerificationError = exports.CryptoService = exports.IPFSService = exports.QrCodeDataService = exports.CommonUtils = exports.StatusesService = exports.VerificationService = exports.CloudflareService = void 0;
 __exportStar(require("./types"), exports);
 __exportStar(require("./utils/common"), exports);
 var CloudflareService_1 = require("./services/CloudflareService");
@@ -26,6 +26,8 @@ var common_1 = require("./utils/common");
 Object.defineProperty(exports, "CommonUtils", { enumerable: true, get: function () { return __importDefault(common_1).default; } });
 var QrCodeDataService_1 = require("./services/QrCodeDataService");
 Object.defineProperty(exports, "QrCodeDataService", { enumerable: true, get: function () { return __importDefault(QrCodeDataService_1).default; } });
+var IPFSService_1 = require("./services/IPFSService");
+Object.defineProperty(exports, "IPFSService", { enumerable: true, get: function () { return __importDefault(IPFSService_1).default; } });
 var CryptoService_1 = require("./services/CryptoService");
 Object.defineProperty(exports, "CryptoService", { enumerable: true, get: function () { return __importDefault(CryptoService_1).default; } });
 var VerificationError_1 = require("./services/VerificationService/VerificationError");
diff --git a/dist/services/CryptoService/CryptoServiceNode.d.ts b/dist/services/CryptoService/CryptoServiceNode.d.ts
index a476eb05720ababc4a9b14f1c39501fb4a8e14c9..9b91b3caabc22a7c3cd53dabfb017c040546a537 100644
--- a/dist/services/CryptoService/CryptoServiceNode.d.ts
+++ b/dist/services/CryptoService/CryptoServiceNode.d.ts
@@ -1,3 +1,19 @@
-import { ICryptoService } from "./ICryptoService";
-declare const implementation: ICryptoService;
-export default implementation;
+/// <reference types="node" />
+import { AESGCMOutput, RSAKeys, ICryptoService } from "./ICryptoService";
+declare class CryptoServiceNode implements ICryptoService {
+    encryptAESGCM(data: string): Promise<AESGCMOutput>;
+    encryptAESGCM(data: ArrayBuffer): Promise<AESGCMOutput>;
+    decryptAESGCM(data: ArrayBuffer, key: ArrayBuffer, iv: ArrayBuffer): Promise<string>;
+    decryptAESGCM(data: ArrayBuffer, key: ArrayBuffer, iv: ArrayBuffer, returnBuffer: true): Promise<ArrayBuffer>;
+    generateRSAKeys(): Promise<RSAKeys>;
+    encryptRSA(publicKeyPEM: string, data: ArrayBuffer): Promise<ArrayBuffer>;
+    decryptRSA(privateKeyPEM: string, data: ArrayBuffer): Promise<ArrayBuffer>;
+    signRSA(privateKeyPEM: string, data: ArrayBuffer): Promise<ArrayBuffer>;
+    SHA1(value: string | ArrayBuffer, encoding?: BufferEncoding): Promise<ArrayBuffer>;
+    SHA256(value: string | ArrayBuffer, encoding?: BufferEncoding): Promise<ArrayBuffer>;
+    SHA384(value: string | ArrayBuffer, encoding?: BufferEncoding): Promise<ArrayBuffer>;
+    SHA512(value: string | ArrayBuffer, encoding?: BufferEncoding): Promise<ArrayBuffer>;
+    MD5(value: string | ArrayBuffer, encoding?: BufferEncoding): Promise<ArrayBuffer>;
+    verifyRSASignature(publicKeyPEM: string, data: ArrayBuffer, signature: ArrayBuffer): Promise<boolean>;
+}
+export default CryptoServiceNode;
diff --git a/dist/services/CryptoService/CryptoServiceNode.js b/dist/services/CryptoService/CryptoServiceNode.js
index 32ab7f691de0342762f57a46716d9968003d3c84..f789e304e709d2a7f8862a7f6d358e83b8b79f9c 100644
--- a/dist/services/CryptoService/CryptoServiceNode.js
+++ b/dist/services/CryptoService/CryptoServiceNode.js
@@ -36,95 +36,6 @@ const js_md5_1 = __importDefault(require("js-md5"));
 const crypto = __importStar(require("crypto"));
 const common_1 = require("../../utils/common");
 const AES_GCM_ALGO = "aes-256-gcm";
-const encryptAESGCM = (data) => __awaiter(void 0, void 0, void 0, function* () {
-    const key = crypto.randomBytes(32);
-    const iv = crypto.randomBytes(12);
-    const cipher = crypto.createCipheriv(AES_GCM_ALGO, key, iv);
-    const encrypted = cipher.update(data, "utf8");
-    cipher.final();
-    const authTag = cipher.getAuthTag();
-    const encryptedWithTag = Buffer.concat([
-        Buffer.from(encrypted),
-        Buffer.from(authTag),
-    ]);
-    return {
-        key: key.buffer,
-        iv: iv.buffer,
-        data: encryptedWithTag,
-    };
-});
-const decryptAESGCM = (data, key, iv) => __awaiter(void 0, void 0, void 0, function* () {
-    const decipher = crypto.createDecipheriv(AES_GCM_ALGO, Buffer.from(key), Buffer.from(iv));
-    const authTag = data.slice(data.byteLength - 16, data.byteLength);
-    const encrypted = data.slice(0, data.byteLength - 16);
-    let str = decipher.update((0, common_1.arrayBufferToBase64)(encrypted), "base64", "utf8");
-    decipher.setAuthTag(Buffer.from(authTag));
-    str += decipher.final("utf8");
-    return str;
-});
-const generateRSAKeys = () => __awaiter(void 0, void 0, void 0, function* () {
-    const { privateKey, publicKey } = crypto.generateKeyPairSync("rsa", {
-        modulusLength: 4096,
-        publicKeyEncoding: {
-            type: "spki",
-            format: "pem",
-        },
-        privateKeyEncoding: {
-            type: "pkcs8",
-            format: "pem",
-        },
-    });
-    return {
-        publicKeyPEM: publicKey,
-        privateKeyPEM: privateKey,
-    };
-});
-const encryptRSA = (publicKeyPEM, data) => __awaiter(void 0, void 0, void 0, function* () {
-    const encryptedData = crypto.publicEncrypt({
-        key: publicKeyPEM,
-        padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
-        oaepHash: "sha256",
-    }, 
-    // We convert the data string to a buffer using `Buffer.from`
-    Buffer.from(data));
-    return encryptedData.buffer;
-});
-const decryptRSA = (privateKeyPEM, data) => __awaiter(void 0, void 0, void 0, function* () {
-    const encryptedData = crypto.privateDecrypt({
-        key: privateKeyPEM,
-        padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
-        oaepHash: "sha256",
-    }, 
-    // We convert the data string to a buffer using `Buffer.from`
-    Buffer.from(data));
-    return encryptedData.buffer;
-});
-const signRSA = (privateKeyPEM, data) => __awaiter(void 0, void 0, void 0, function* () {
-    const sign = crypto.createSign("SHA256");
-    sign.update(Buffer.from(data));
-    sign.end();
-    return sign.sign(privateKeyPEM).buffer;
-});
-const SHA1 = (value, encoding = "utf8") => __awaiter(void 0, void 0, void 0, function* () {
-    const bytes = getBytes(value, encoding);
-    return crypto.createHash("sha1").update(bytes).digest();
-});
-const SHA256 = (value, encoding = "utf8") => __awaiter(void 0, void 0, void 0, function* () {
-    const bytes = getBytes(value, encoding);
-    return crypto.createHash("sha256").update(bytes).digest();
-});
-const SHA384 = (value, encoding = "utf8") => __awaiter(void 0, void 0, void 0, function* () {
-    const bytes = getBytes(value, encoding);
-    return crypto.createHash("sha384").update(bytes).digest();
-});
-const SHA512 = (value, encoding = "utf8") => __awaiter(void 0, void 0, void 0, function* () {
-    const bytes = getBytes(value, encoding);
-    return crypto.createHash("sha512").update(bytes).digest();
-});
-const MD5 = (value, encoding = "utf8") => __awaiter(void 0, void 0, void 0, function* () {
-    const bytes = getBytes(value, encoding);
-    return js_md5_1.default.arrayBuffer(bytes);
-});
 const getBytes = (value, encoding) => {
     let bytes;
     if (typeof value === "string") {
@@ -136,25 +47,137 @@ const getBytes = (value, encoding) => {
     }
     return bytes;
 };
-const verifyRSASignature = (publicKeyPEM, data, signature) => __awaiter(void 0, void 0, void 0, function* () {
-    const verify = crypto.createVerify("SHA256");
-    verify.update(Buffer.from(data));
-    verify.end();
-    const publicKey = crypto.createPublicKey(Buffer.from(publicKeyPEM, "utf-8"));
-    return verify.verify(publicKey, Buffer.from(signature));
-});
-const implementation = {
-    encryptAESGCM,
-    decryptAESGCM,
-    generateRSAKeys,
-    encryptRSA,
-    decryptRSA,
-    SHA256,
-    signRSA,
-    verifyRSASignature,
-    SHA512,
-    SHA384,
-    SHA1,
-    MD5,
-};
-exports.default = implementation;
+class CryptoServiceNode {
+    encryptAESGCM(data) {
+        return __awaiter(this, void 0, void 0, function* () {
+            const key = crypto.randomBytes(32);
+            const iv = crypto.randomBytes(12);
+            const cipher = crypto.createCipheriv(AES_GCM_ALGO, key, iv);
+            let encrypted;
+            if (typeof data === "string") {
+                encrypted = cipher.update(data, "utf8");
+            }
+            else {
+                encrypted = cipher.update((0, common_1.ensureUint8Array)(data));
+            }
+            cipher.final();
+            const authTag = cipher.getAuthTag();
+            const encryptedWithTag = Buffer.concat([
+                Buffer.from(encrypted),
+                Buffer.from(authTag),
+            ]);
+            return {
+                key: key.buffer,
+                iv: iv.buffer,
+                data: encryptedWithTag,
+            };
+        });
+    }
+    decryptAESGCM(data, key, iv, returnBuffer) {
+        return __awaiter(this, void 0, void 0, function* () {
+            const decipher = crypto.createDecipheriv(AES_GCM_ALGO, Buffer.from(key), Buffer.from(iv));
+            const authTag = data.slice(data.byteLength - 16, data.byteLength);
+            const encrypted = data.slice(0, data.byteLength - 16);
+            if (returnBuffer) {
+                const decrypted = decipher.update((0, common_1.ensureUint8Array)(encrypted));
+                decipher.setAuthTag(Buffer.from(authTag));
+                return Buffer.concat([decrypted, decipher.final()]);
+            }
+            let str = decipher.update((0, common_1.ensureUint8Array)(encrypted), null, "utf8");
+            decipher.setAuthTag(Buffer.from(authTag));
+            str += decipher.final("utf8");
+            return str;
+        });
+    }
+    generateRSAKeys() {
+        return __awaiter(this, void 0, void 0, function* () {
+            const { privateKey, publicKey } = crypto.generateKeyPairSync("rsa", {
+                modulusLength: 4096,
+                publicKeyEncoding: {
+                    type: "spki",
+                    format: "pem",
+                },
+                privateKeyEncoding: {
+                    type: "pkcs8",
+                    format: "pem",
+                },
+            });
+            return {
+                publicKeyPEM: publicKey,
+                privateKeyPEM: privateKey,
+            };
+        });
+    }
+    encryptRSA(publicKeyPEM, data) {
+        return __awaiter(this, void 0, void 0, function* () {
+            const encryptedData = crypto.publicEncrypt({
+                key: publicKeyPEM,
+                padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
+                oaepHash: "sha256",
+            }, 
+            // We convert the data string to a buffer using `Buffer.from`
+            Buffer.from(data));
+            return encryptedData.buffer;
+        });
+    }
+    decryptRSA(privateKeyPEM, data) {
+        return __awaiter(this, void 0, void 0, function* () {
+            const encryptedData = crypto.privateDecrypt({
+                key: privateKeyPEM,
+                padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
+                oaepHash: "sha256",
+            }, 
+            // We convert the data string to a buffer using `Buffer.from`
+            Buffer.from(data));
+            return encryptedData.buffer;
+        });
+    }
+    signRSA(privateKeyPEM, data) {
+        return __awaiter(this, void 0, void 0, function* () {
+            const sign = crypto.createSign("SHA256");
+            sign.update(Buffer.from(data));
+            sign.end();
+            return sign.sign(privateKeyPEM).buffer;
+        });
+    }
+    SHA1(value, encoding = "utf8") {
+        return __awaiter(this, void 0, void 0, function* () {
+            const bytes = getBytes(value, encoding);
+            return crypto.createHash("sha1").update(bytes).digest();
+        });
+    }
+    SHA256(value, encoding = "utf8") {
+        return __awaiter(this, void 0, void 0, function* () {
+            const bytes = getBytes(value, encoding);
+            return crypto.createHash("sha256").update(bytes).digest();
+        });
+    }
+    SHA384(value, encoding = "utf8") {
+        return __awaiter(this, void 0, void 0, function* () {
+            const bytes = getBytes(value, encoding);
+            return crypto.createHash("sha384").update(bytes).digest();
+        });
+    }
+    SHA512(value, encoding = "utf8") {
+        return __awaiter(this, void 0, void 0, function* () {
+            const bytes = getBytes(value, encoding);
+            return crypto.createHash("sha512").update(bytes).digest();
+        });
+    }
+    MD5(value, encoding = "utf8") {
+        return __awaiter(this, void 0, void 0, function* () {
+            const bytes = getBytes(value, encoding);
+            return js_md5_1.default.arrayBuffer(bytes);
+        });
+    }
+    verifyRSASignature(publicKeyPEM, data, signature) {
+        return __awaiter(this, void 0, void 0, function* () {
+            const verify = crypto.createVerify("SHA256");
+            verify.update(Buffer.from(data));
+            verify.end();
+            const publicKey = crypto.createPublicKey(Buffer.from(publicKeyPEM, "utf-8"));
+            return verify.verify(publicKey, Buffer.from(signature));
+        });
+    }
+}
+exports.default = CryptoServiceNode;
diff --git a/dist/services/CryptoService/CryptoServiceWeb.d.ts b/dist/services/CryptoService/CryptoServiceWeb.d.ts
index 7fdeee9d024facc4f65ac4610858a01e283d5b80..3ab743beca0bad3313a4e703de3d659fcd3aaa02 100644
--- a/dist/services/CryptoService/CryptoServiceWeb.d.ts
+++ b/dist/services/CryptoService/CryptoServiceWeb.d.ts
@@ -1,4 +1,18 @@
-import { ICryptoService } from "./ICryptoService";
-export declare const verifyRSASignature: (publicKeyPEM: string, data: ArrayBuffer, signature: ArrayBuffer) => Promise<boolean>;
-declare const implementation: ICryptoService;
-export default implementation;
+import { AESGCMOutput, ICryptoService, RSAKeys } from "./ICryptoService";
+declare class CryptoServiceWeb implements ICryptoService {
+    encryptAESGCM(data: string): Promise<AESGCMOutput>;
+    encryptAESGCM(data: ArrayBuffer): Promise<AESGCMOutput>;
+    decryptAESGCM(data: ArrayBuffer, key: ArrayBuffer, iv: ArrayBuffer): Promise<string>;
+    decryptAESGCM(data: ArrayBuffer, key: ArrayBuffer, iv: ArrayBuffer, returnBuffer: true): Promise<ArrayBuffer>;
+    verifyRSASignature(publicKeyPEM: string, data: ArrayBuffer, signature: ArrayBuffer): Promise<boolean>;
+    generateRSAKeys(): Promise<RSAKeys>;
+    encryptRSA(publicKeyPEM: string, data: ArrayBuffer): Promise<ArrayBuffer>;
+    decryptRSA(privateKeyPEM: string, data: ArrayBuffer): Promise<ArrayBuffer>;
+    signRSA(privateKeyPEM: string, data: ArrayBuffer): Promise<ArrayBuffer>;
+    SHA1(value: string | ArrayBuffer, encoding?: string): Promise<ArrayBuffer>;
+    SHA256(value: string | ArrayBuffer, encoding?: string): Promise<ArrayBuffer>;
+    SHA384(value: string | ArrayBuffer, encoding?: string): Promise<ArrayBuffer>;
+    SHA512(value: string | ArrayBuffer, encoding?: string): Promise<ArrayBuffer>;
+    MD5(value: string | ArrayBuffer, encoding?: string): Promise<ArrayBuffer>;
+}
+export default CryptoServiceWeb;
diff --git a/dist/services/CryptoService/CryptoServiceWeb.js b/dist/services/CryptoService/CryptoServiceWeb.js
index 2164c6a64f0e3ae2731ab24415334e4a72331824..be96a6ee078c2a019054b85e3bf832e26ede3620 100644
--- a/dist/services/CryptoService/CryptoServiceWeb.js
+++ b/dist/services/CryptoService/CryptoServiceWeb.js
@@ -12,14 +12,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
     return (mod && mod.__esModule) ? mod : { "default": mod };
 };
 Object.defineProperty(exports, "__esModule", { value: true });
-exports.verifyRSASignature = void 0;
 const js_md5_1 = __importDefault(require("js-md5"));
 const common_1 = require("../../utils/common");
-function exportKey(key) {
-    return __awaiter(this, void 0, void 0, function* () {
-        return crypto.subtle.exportKey("raw", key);
-    });
-}
+const exportKey = (key) => __awaiter(void 0, void 0, void 0, function* () {
+    return crypto.subtle.exportKey("raw", key);
+});
 const convertPemToBinary = (pem) => {
     const lines = pem.split("\n");
     let encoded = "";
@@ -34,64 +31,6 @@ const convertPemToBinary = (pem) => {
     }
     return (0, common_1.base64ToArrayBuffer)(encoded);
 };
-const encryptRSA = (publicKeyPEM, data) => __awaiter(void 0, void 0, void 0, function* () {
-    const publicKey = yield crypto.subtle.importKey("spki", convertPemToBinary(publicKeyPEM), {
-        name: "RSA-OAEP",
-        hash: "SHA-256",
-    }, true, ["encrypt"]);
-    return crypto.subtle.encrypt({
-        name: "RSA-OAEP",
-    }, publicKey, data);
-});
-const verifyRSASignature = (publicKeyPEM, data, signature) => __awaiter(void 0, void 0, void 0, function* () {
-    const publicKey = yield crypto.subtle.importKey("spki", convertPemToBinary(publicKeyPEM), {
-        name: "RSASSA-PKCS1-v1_5",
-        hash: "SHA-256",
-    }, true, ["verify"]);
-    return yield crypto.subtle.verify({
-        name: "RSASSA-PKCS1-v1_5",
-        hash: "SHA-256",
-    }, publicKey, signature, data);
-});
-exports.verifyRSASignature = verifyRSASignature;
-const encryptAESGCM = (data) => __awaiter(void 0, void 0, void 0, function* () {
-    const key = yield crypto.subtle.generateKey({
-        name: "AES-GCM",
-        length: 256,
-    }, true, ["encrypt", "decrypt"]);
-    const encoded = new TextEncoder().encode(data);
-    const iv = crypto.getRandomValues(new Buffer(12));
-    const encrypted = yield crypto.subtle.encrypt({ name: "AES-GCM", iv: iv }, key, encoded);
-    return { data: encrypted, key: yield exportKey(key), iv };
-});
-const decryptAESGCM = (data, key, iv) => __awaiter(void 0, void 0, void 0, function* () {
-    const importedKey = yield crypto.subtle.importKey("raw", key, {
-        name: "AES-GCM",
-        length: 256,
-    }, true, ["encrypt", "decrypt"]);
-    const decrypted = yield crypto.subtle.decrypt({ name: "AES-GCM", iv: iv }, importedKey, data);
-    return new TextDecoder().decode(decrypted);
-});
-const SHA1 = (value, encoding = "utf8") => __awaiter(void 0, void 0, void 0, function* () {
-    const bytes = getBytes(value, encoding);
-    return yield crypto.subtle.digest("SHA-1", bytes);
-});
-const SHA256 = (value, encoding = "utf8") => __awaiter(void 0, void 0, void 0, function* () {
-    const bytes = getBytes(value, encoding);
-    return yield crypto.subtle.digest("SHA-256", bytes);
-});
-const SHA384 = (value, encoding = "utf8") => __awaiter(void 0, void 0, void 0, function* () {
-    const bytes = getBytes(value, encoding);
-    return yield crypto.subtle.digest("SHA-384", bytes);
-});
-const SHA512 = (value, encoding = "utf8") => __awaiter(void 0, void 0, void 0, function* () {
-    const bytes = getBytes(value, encoding);
-    return yield crypto.subtle.digest("SHA-512", bytes);
-});
-const MD5 = (value, encoding = "utf8") => __awaiter(void 0, void 0, void 0, function* () {
-    const bytes = getBytes(value, encoding);
-    return js_md5_1.default.arrayBuffer(bytes);
-});
 const getBytes = (value, encoding) => {
     let bytes;
     if (typeof value === "string") {
@@ -108,24 +47,105 @@ const getBytes = (value, encoding) => {
     }
     return bytes;
 };
-const implementation = {
-    encryptAESGCM,
-    decryptAESGCM,
-    verifyRSASignature: exports.verifyRSASignature,
+class CryptoServiceWeb {
+    encryptAESGCM(data) {
+        return __awaiter(this, void 0, void 0, function* () {
+            const key = yield crypto.subtle.generateKey({
+                name: "AES-GCM",
+                length: 256,
+            }, true, ["encrypt", "decrypt"]);
+            let encoded;
+            if (typeof data === "string") {
+                encoded = new TextEncoder().encode(data);
+            }
+            else {
+                encoded = data;
+            }
+            const iv = crypto.getRandomValues(new Buffer(12));
+            const encrypted = yield crypto.subtle.encrypt({ name: "AES-GCM", iv: iv }, key, encoded);
+            return { data: encrypted, key: yield exportKey(key), iv };
+        });
+    }
+    decryptAESGCM(data, key, iv, returnBuffer) {
+        return __awaiter(this, void 0, void 0, function* () {
+            const importedKey = yield crypto.subtle.importKey("raw", key, {
+                name: "AES-GCM",
+                length: 256,
+            }, true, ["encrypt", "decrypt"]);
+            const decrypted = yield crypto.subtle.decrypt({ name: "AES-GCM", iv: iv }, importedKey, data);
+            if (returnBuffer) {
+                return decrypted;
+            }
+            return new TextDecoder().decode(decrypted);
+        });
+    }
+    verifyRSASignature(publicKeyPEM, data, signature) {
+        return __awaiter(this, void 0, void 0, function* () {
+            const publicKey = yield crypto.subtle.importKey("spki", convertPemToBinary(publicKeyPEM), {
+                name: "RSASSA-PKCS1-v1_5",
+                hash: "SHA-256",
+            }, true, ["verify"]);
+            return yield crypto.subtle.verify({
+                name: "RSASSA-PKCS1-v1_5",
+                hash: "SHA-256",
+            }, publicKey, signature, data);
+        });
+    }
     generateRSAKeys() {
-        throw new Error("Surprise. Not implemented yet :)");
-    },
-    encryptRSA,
+        return __awaiter(this, void 0, void 0, function* () {
+            throw new Error("The function is not implemented");
+        });
+    }
+    encryptRSA(publicKeyPEM, data) {
+        return __awaiter(this, void 0, void 0, function* () {
+            const publicKey = yield crypto.subtle.importKey("spki", convertPemToBinary(publicKeyPEM), {
+                name: "RSA-OAEP",
+                hash: "SHA-256",
+            }, true, ["encrypt"]);
+            return crypto.subtle.encrypt({
+                name: "RSA-OAEP",
+            }, publicKey, data);
+        });
+    }
     decryptRSA(privateKeyPEM, data) {
-        throw new Error("Surprise. Not implemented yet :)");
-    },
+        return __awaiter(this, void 0, void 0, function* () {
+            throw new Error("The function is not implemented");
+        });
+    }
     signRSA(privateKeyPEM, data) {
-        throw new Error("Surprise. Not implemented yet :)");
-    },
-    SHA1,
-    SHA384,
-    SHA256,
-    SHA512,
-    MD5,
-};
-exports.default = implementation;
+        return __awaiter(this, void 0, void 0, function* () {
+            throw new Error("The function is not implemented");
+        });
+    }
+    SHA1(value, encoding = "utf8") {
+        return __awaiter(this, void 0, void 0, function* () {
+            const bytes = getBytes(value, encoding);
+            return yield crypto.subtle.digest("SHA-1", bytes);
+        });
+    }
+    SHA256(value, encoding = "utf8") {
+        return __awaiter(this, void 0, void 0, function* () {
+            const bytes = getBytes(value, encoding);
+            return yield crypto.subtle.digest("SHA-256", bytes);
+        });
+    }
+    SHA384(value, encoding = "utf8") {
+        return __awaiter(this, void 0, void 0, function* () {
+            const bytes = getBytes(value, encoding);
+            return yield crypto.subtle.digest("SHA-384", bytes);
+        });
+    }
+    SHA512(value, encoding = "utf8") {
+        return __awaiter(this, void 0, void 0, function* () {
+            const bytes = getBytes(value, encoding);
+            return yield crypto.subtle.digest("SHA-512", bytes);
+        });
+    }
+    MD5(value, encoding = "utf8") {
+        return __awaiter(this, void 0, void 0, function* () {
+            const bytes = getBytes(value, encoding);
+            return js_md5_1.default.arrayBuffer(bytes);
+        });
+    }
+}
+exports.default = CryptoServiceWeb;
diff --git a/dist/services/CryptoService/ICryptoService.d.ts b/dist/services/CryptoService/ICryptoService.d.ts
index cefd7591fd036f4e6ae6a77339d86a2ec9f5abbe..24dfcd1294c69e9cb0ef9035e48651b8936c8d28 100644
--- a/dist/services/CryptoService/ICryptoService.d.ts
+++ b/dist/services/CryptoService/ICryptoService.d.ts
@@ -8,8 +8,14 @@ export interface RSAKeys {
     privateKeyPEM: string;
 }
 export interface ICryptoService {
-    encryptAESGCM: (data: string) => Promise<AESGCMOutput>;
-    decryptAESGCM: (data: ArrayBuffer, key: ArrayBuffer, iv: ArrayBuffer) => Promise<string>;
+    encryptAESGCM: {
+        (data: string): Promise<AESGCMOutput>;
+        (data: ArrayBuffer): Promise<AESGCMOutput>;
+    };
+    decryptAESGCM: {
+        (data: ArrayBuffer, key: ArrayBuffer, iv: ArrayBuffer): Promise<string>;
+        (data: ArrayBuffer, key: ArrayBuffer, iv: ArrayBuffer, returnBuffer: true): Promise<ArrayBuffer>;
+    };
     generateRSAKeys: () => Promise<RSAKeys>;
     encryptRSA: (publicKeyPEM: string, data: ArrayBuffer) => Promise<ArrayBuffer>;
     decryptRSA: (privateKeyPEM: string, data: ArrayBuffer) => Promise<ArrayBuffer>;
diff --git a/dist/services/CryptoService/index.d.ts b/dist/services/CryptoService/index.d.ts
index 9db9c1b552f148203b72746e7994957cce6ab74f..a3ce65fa922fdeddd643b143d3ea70640ade2876 100644
--- a/dist/services/CryptoService/index.d.ts
+++ b/dist/services/CryptoService/index.d.ts
@@ -1,2 +1,3 @@
-declare const service: import("./ICryptoService").ICryptoService;
+import { ICryptoService } from "./ICryptoService";
+declare const service: ICryptoService;
 export default service;
diff --git a/dist/services/CryptoService/index.js b/dist/services/CryptoService/index.js
index ce863013f23d371a28f88872926e9ac1b53f39f1..cbea690370e0b558d5a46f7320a362902f8d5ac0 100644
--- a/dist/services/CryptoService/index.js
+++ b/dist/services/CryptoService/index.js
@@ -6,6 +6,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
 const CryptoServiceNode_1 = __importDefault(require("./CryptoServiceNode"));
 const CryptoServiceWeb_1 = __importDefault(require("./CryptoServiceWeb"));
 const service = typeof crypto !== "undefined" && crypto.subtle
-    ? CryptoServiceWeb_1.default
-    : CryptoServiceNode_1.default;
+    ? new CryptoServiceWeb_1.default()
+    : new CryptoServiceNode_1.default();
 exports.default = service;
diff --git a/dist/services/IPFSService.d.ts b/dist/services/IPFSService.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f9328844c37049ced91b030476b601d5b693adc3
--- /dev/null
+++ b/dist/services/IPFSService.d.ts
@@ -0,0 +1,9 @@
+import { IpfsContentData, IpfsDataVersion } from "../types";
+declare class IPFSDataRetriever {
+    ipfsUrl: any;
+    constructor(ipfsUrl: string);
+    getDecodedContent(contentData: IpfsContentData, ipfsVersion: IpfsDataVersion): Promise<ArrayBuffer>;
+    private getRawContent;
+    private decryptContent;
+}
+export default IPFSDataRetriever;
diff --git a/dist/services/IPFSService.js b/dist/services/IPFSService.js
new file mode 100644
index 0000000000000000000000000000000000000000..72d5a8ad30529d2fb3765d5869d81ef4ffe0f154
--- /dev/null
+++ b/dist/services/IPFSService.js
@@ -0,0 +1,66 @@
+"use strict";
+var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
+    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
+    return new (P || (P = Promise))(function (resolve, reject) {
+        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
+        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
+        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
+        step((generator = generator.apply(thisArg, _arguments || [])).next());
+    });
+};
+var __importDefault = (this && this.__importDefault) || function (mod) {
+    return (mod && mod.__esModule) ? mod : { "default": mod };
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+const axios_1 = __importDefault(require("axios"));
+const common_1 = require("../utils/common");
+const QrCodeDataService_1 = __importDefault(require("./QrCodeDataService"));
+const CryptoService_1 = __importDefault(require("./CryptoService"));
+const GET_CONTENT_PATH = "/ipfs";
+class IPFSDataRetriever {
+    constructor(ipfsUrl) {
+        this.ipfsUrl = ipfsUrl;
+    }
+    getDecodedContent(contentData, ipfsVersion) {
+        return __awaiter(this, void 0, void 0, function* () {
+            const rawContent = yield this.getRawContent(contentData.cid, contentData.head, ipfsVersion);
+            return this.decryptContent(rawContent, contentData.key);
+        });
+    }
+    getRawContent(cid, head, ipfsVersion) {
+        return __awaiter(this, void 0, void 0, function* () {
+            let assembledData;
+            if (cid) {
+                let ipfsFileContent;
+                if (ipfsVersion === undefined) {
+                    const response = yield (0, axios_1.default)({
+                        url: `${this.ipfsUrl}${GET_CONTENT_PATH}/${cid}`,
+                        method: "GET",
+                    });
+                    ipfsFileContent = (0, common_1.base64ToArrayBuffer)(response.data);
+                }
+                else if (ipfsVersion === "v2") {
+                    const result = yield axios_1.default.get(`${this.ipfsUrl}${GET_CONTENT_PATH}/${cid}`, { responseType: "arraybuffer" });
+                    ipfsFileContent = Buffer.from(result.data);
+                }
+                else {
+                    throw new Error("Unknown qrcode ipfs version");
+                }
+                assembledData = QrCodeDataService_1.default.assembleQrCodeData((0, common_1.base64ToArrayBuffer)(head), ipfsFileContent);
+            }
+            else {
+                assembledData = (0, common_1.base64ToArrayBuffer)(head);
+            }
+            return assembledData;
+        });
+    }
+    decryptContent(content, decryptionKey) {
+        return __awaiter(this, void 0, void 0, function* () {
+            const { key, data: iv } = QrCodeDataService_1.default.decodeKeyDataPair(decryptionKey);
+            const decryptedZip = yield CryptoService_1.default.decryptAESGCM(content, (0, common_1.base64ToArrayBuffer)(key), (0, common_1.base64ToArrayBuffer)(iv), true);
+            const decompressed = (0, common_1.decompressData)(decryptedZip);
+            return decompressed;
+        });
+    }
+}
+exports.default = IPFSDataRetriever;
diff --git a/dist/services/QrCodeDataService.d.ts b/dist/services/QrCodeDataService.d.ts
index fc6435561bce0f97009745bf8cc3536ef64033e0..1babbf049d295d8854ecdcda67ad720e4fc8a1b0 100644
--- a/dist/services/QrCodeDataService.d.ts
+++ b/dist/services/QrCodeDataService.d.ts
@@ -1,9 +1,4 @@
 import { KeyDataPair, MessageData } from "../types";
-import CloudflareService from "./CloudflareService";
-import { ICryptoService } from "./CryptoService/ICryptoService";
-interface WithServices {
-    getMessageDataFromBase64: (base64: string) => Promise<MessageData>;
-}
 declare const _default: {
     encodeEmailData: (emailData: MessageData) => string;
     decodeEmailData: (binary: string | ArrayBuffer | Uint8Array) => MessageData;
@@ -14,9 +9,7 @@ declare const _default: {
         tail: ArrayBuffer;
     };
     assembleQrCodeData: (head: ArrayBuffer, tail: ArrayBuffer) => ArrayBuffer;
-    computeQrCodeHash: (emailData: MessageData) => Promise<string>;
     calculateQRCodeSignature: (accountPrivateKey: string, qrCodeHash: string) => Promise<string>;
     verifyQrCodeSignature: (publicKey: string, qrCodeSignature: string, recipientQRCodeHash: string) => Promise<boolean>;
-    withServices: (cloudFlareServiceInstance: CloudflareService, landingPageServiceUrl: string, cryptoService?: ICryptoService) => WithServices;
 };
 export default _default;
diff --git a/dist/services/QrCodeDataService.js b/dist/services/QrCodeDataService.js
index 955f842a65a54792f93146d0bc74674d786ddb9d..1c668db6c7598507da2882538839fcad47506bd2 100644
--- a/dist/services/QrCodeDataService.js
+++ b/dist/services/QrCodeDataService.js
@@ -8,14 +8,9 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
         step((generator = generator.apply(thisArg, _arguments || [])).next());
     });
 };
-var __importDefault = (this && this.__importDefault) || function (mod) {
-    return (mod && mod.__esModule) ? mod : { "default": mod };
-};
 Object.defineProperty(exports, "__esModule", { value: true });
-const axios_1 = __importDefault(require("axios"));
 const qrcode_data_pb_1 = require("../generated/qrcode_data_pb");
 const common_1 = require("../utils/common");
-const CryptoService_1 = __importDefault(require("./CryptoService"));
 const index_1 = require("../index");
 const EmailDataMessageV1 = qrcode_data_pb_1.vereign.protobuf.qrcode_data.EmailData_V1;
 const KeyDataMessageV1 = qrcode_data_pb_1.vereign.protobuf.qrcode_data.KeyData_V1;
@@ -78,49 +73,6 @@ const breakQrCodeData = (data, headBytesSize = 32) => {
 const assembleQrCodeData = (head, tail) => {
     return Buffer.concat([Buffer.from(head), Buffer.from(tail)]);
 };
-const computeQrCodeHash = (emailData) => __awaiter(void 0, void 0, void 0, function* () {
-    let attachments = [];
-    if (emailData.attachments) {
-        attachments = emailData.attachments.map((attachment) => {
-            //TODO: check if we can use attachment hash, attachment hashAlg
-            if (attachment.url) {
-                return {
-                    name: attachment.name,
-                    size: attachment.size,
-                    url: attachment.url,
-                };
-            }
-            return {
-                name: attachment.name,
-                size: attachment.size,
-            };
-        });
-    }
-    const dataForHashing = {
-        sender: emailData.sender,
-        subject: emailData.subject,
-        recipients: emailData.recipients,
-        attachments,
-    };
-    const promises = Object.values(dataForHashing).map((value) => {
-        const string = Buffer.from(JSON.stringify(value, (key, value) => value instanceof Object && !(value instanceof Array)
-            ? Object.keys(value)
-                .sort()
-                .reduce((sorted, key) => {
-                sorted[key] = value[key];
-                return sorted;
-            }, {})
-            : value)).toString();
-        return index_1.CryptoService.SHA256(string);
-    });
-    const hashArray = yield Promise.all(promises);
-    const hashesAsAstring = hashArray
-        .map(common_1.arrayBufferToBase64)
-        .sort()
-        .join("\n");
-    const resultBuffer = yield index_1.CryptoService.SHA256(hashesAsAstring);
-    return (0, common_1.arrayBufferToBase64)(resultBuffer);
-});
 const calculateQRCodeSignature = (accountPrivateKey, qrCodeHash) => __awaiter(void 0, void 0, void 0, function* () {
     const signature = yield index_1.CryptoService.signRSA(accountPrivateKey, Buffer.from(qrCodeHash));
     return (0, common_1.arrayBufferToBase64)(signature);
@@ -131,46 +83,6 @@ const verifyQrCodeSignature = (publicKey, qrCodeSignature, recipientQRCodeHash)
     const result = yield index_1.CryptoService.verifyRSASignature(publicKey, recipientQRCodeHashAsBuffer, qrCodeSignatureAsBuffer);
     return result;
 });
-/**
- * Higher order function to perform complex operations with the QR code data
- * @param cloudFlareServiceInstance = new CloudflareService("CDN_RUL", "DEFAULT_BUCKET")
- * @param landingPageServiceUrl - URL of the service responsible for RSA decryption
- * @param cryptoService - instance of ICryptoService. Override only needed cryptographic functions.
- * The rest will be filled with default ones from CryptoServiceWeb/CryptoServiceNode
- */
-const withServices = (cloudFlareServiceInstance, landingPageServiceUrl, cryptoService) => {
-    if (cryptoService) {
-        cryptoService = Object.assign(Object.assign({}, CryptoService_1.default), cryptoService);
-    }
-    else {
-        cryptoService = CryptoService_1.default;
-    }
-    return {
-        getMessageDataFromBase64: (base64) => __awaiter(void 0, void 0, void 0, function* () {
-            const qrCodeDataPart = decodeKeyDataPair(base64);
-            const base64SHA256 = yield cryptoService.SHA256(base64);
-            const { data: backblazeDataPart, } = yield cloudFlareServiceInstance.fetchFile(`qrcode-${(0, common_1.arrayBufferToHex)(base64SHA256)}`);
-            const assembledData = assembleQrCodeData((0, common_1.base64ToArrayBuffer)(qrCodeDataPart.data), (0, common_1.base64ToArrayBuffer)(backblazeDataPart.qr_code_data));
-            const { key: aesEncryptedSessionKey, data: storageIv, } = decodeKeyDataPair(backblazeDataPart.session_key);
-            const rsaEncryptedSessionKey = yield cryptoService.decryptAESGCM((0, common_1.base64ToArrayBuffer)(aesEncryptedSessionKey), (0, common_1.base64ToArrayBuffer)(qrCodeDataPart.key), (0, common_1.base64ToArrayBuffer)(storageIv));
-            const { data: { key: encodedSessionKey }, } = yield (0, axios_1.default)({
-                url: `${landingPageServiceUrl}/api/hsm/decrypt`,
-                method: "POST",
-                headers: {
-                    "Content-Type": "application/json",
-                },
-                data: {
-                    key_signature: backblazeDataPart.key_signature,
-                    encrypted_key: rsaEncryptedSessionKey,
-                },
-            });
-            const decodedSessionKey = decodeKeyDataPair(encodedSessionKey);
-            const decryptedEmailData = yield cryptoService.decryptAESGCM(assembledData, (0, common_1.base64ToArrayBuffer)(decodedSessionKey.key), (0, common_1.base64ToArrayBuffer)(decodedSessionKey.data));
-            const decompressedEmailData = (0, common_1.decompressData)(decryptedEmailData);
-            return decodeEmailData(decompressedEmailData);
-        }),
-    };
-};
 exports.default = {
     encodeEmailData,
     decodeEmailData,
@@ -178,8 +90,6 @@ exports.default = {
     decodeKeyDataPair,
     breakQrCodeData,
     assembleQrCodeData,
-    computeQrCodeHash,
     calculateQRCodeSignature,
     verifyQrCodeSignature,
-    withServices,
 };
diff --git a/dist/types.d.ts b/dist/types.d.ts
index dd2fbf38a1276a0c8a2eec7e2327bcf340372717..e346f229980231919afc52f9b8194e0af5367087 100644
--- a/dist/types.d.ts
+++ b/dist/types.d.ts
@@ -21,8 +21,11 @@ export interface IpfsAttachmentData {
     key: string;
     contentHash: string;
     head: string;
+    contentType: string;
 }
+export declare type IpfsDataVersion = undefined | "v2";
 export interface IpfsData {
+    version: IpfsDataVersion;
     plaintText: IpfsContentData;
     html: IpfsContentData;
     attachments: Array<IpfsAttachmentData>;
diff --git a/src/index.ts b/src/index.ts
index 348a83c2e2ad5063b65da49d3bfba64274514e81..c116b57ea3b5d35efc783c395e9c6dcd01b755bd 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -5,6 +5,7 @@ export { default as VerificationService } from "./services/VerificationService";
 export { default as StatusesService } from "./services/StatusesService";
 export { default as CommonUtils } from "./utils/common";
 export { default as QrCodeDataService } from "./services/QrCodeDataService";
+export { default as IPFSService } from "./services/IPFSService";
 export { default as CryptoService } from "./services/CryptoService";
 export { default as VerificationError } from "./services/VerificationService/VerificationError";
 export { default as QrCodeTemplate } from "./utils/qrCodeTemplateUtils";
diff --git a/src/services/CryptoService/CryptoServiceNode.ts b/src/services/CryptoService/CryptoServiceNode.ts
index b776a02640a1de9ebc9714463ecd36ae1d750b61..c98ecf2187558d3e30df438d1601e3ed6de46d41 100644
--- a/src/services/CryptoService/CryptoServiceNode.ts
+++ b/src/services/CryptoService/CryptoServiceNode.ts
@@ -4,154 +4,10 @@ import md5 from "js-md5";
 import * as crypto from "crypto";
 
 import { AESGCMOutput, RSAKeys, ICryptoService } from "./ICryptoService";
-import { arrayBufferToBase64 } from "../../utils/common";
+import { ensureUint8Array } from "../../utils/common";
 
 const AES_GCM_ALGO = "aes-256-gcm";
 
-const encryptAESGCM = async (data: string): Promise<AESGCMOutput> => {
-  const key = crypto.randomBytes(32);
-  const iv = crypto.randomBytes(12);
-  const cipher = crypto.createCipheriv(AES_GCM_ALGO, key, iv);
-
-  const encrypted = cipher.update(data, "utf8");
-  cipher.final();
-
-  const authTag = cipher.getAuthTag();
-  const encryptedWithTag = Buffer.concat([
-    Buffer.from(encrypted),
-    Buffer.from(authTag),
-  ]);
-
-  return {
-    key: key.buffer,
-    iv: iv.buffer,
-    data: encryptedWithTag,
-  };
-};
-
-const decryptAESGCM = async (
-  data: ArrayBuffer,
-  key: ArrayBuffer,
-  iv: ArrayBuffer
-): Promise<string> => {
-  const decipher = crypto.createDecipheriv(
-    AES_GCM_ALGO,
-    Buffer.from(key),
-    Buffer.from(iv)
-  );
-
-  const authTag = data.slice(data.byteLength - 16, data.byteLength);
-  const encrypted = data.slice(0, data.byteLength - 16);
-  let str = decipher.update(arrayBufferToBase64(encrypted), "base64", "utf8");
-  decipher.setAuthTag(Buffer.from(authTag));
-  str += decipher.final("utf8");
-  return str;
-};
-
-const generateRSAKeys = async (): Promise<RSAKeys> => {
-  const { privateKey, publicKey } = crypto.generateKeyPairSync("rsa", {
-    modulusLength: 4096,
-    publicKeyEncoding: {
-      type: "spki",
-      format: "pem",
-    },
-    privateKeyEncoding: {
-      type: "pkcs8",
-      format: "pem",
-    },
-  });
-
-  return {
-    publicKeyPEM: publicKey,
-    privateKeyPEM: privateKey,
-  };
-};
-
-const encryptRSA = async (
-  publicKeyPEM: string,
-  data: ArrayBuffer
-): Promise<ArrayBuffer> => {
-  const encryptedData = crypto.publicEncrypt(
-    {
-      key: publicKeyPEM,
-      padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
-      oaepHash: "sha256",
-    },
-    // We convert the data string to a buffer using `Buffer.from`
-    Buffer.from(data)
-  );
-
-  return encryptedData.buffer;
-};
-
-const decryptRSA = async (
-  privateKeyPEM: string,
-  data: ArrayBuffer
-): Promise<ArrayBuffer> => {
-  const encryptedData = crypto.privateDecrypt(
-    {
-      key: privateKeyPEM,
-      padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
-      oaepHash: "sha256",
-    },
-    // We convert the data string to a buffer using `Buffer.from`
-    Buffer.from(data)
-  );
-
-  return encryptedData.buffer;
-};
-
-const signRSA = async (
-  privateKeyPEM: string,
-  data: ArrayBuffer
-): Promise<ArrayBuffer> => {
-  const sign = crypto.createSign("SHA256");
-  sign.update(Buffer.from(data));
-  sign.end();
-
-  return sign.sign(privateKeyPEM).buffer;
-};
-
-const SHA1 = async (
-  value: string | ArrayBuffer,
-  encoding: BufferEncoding = "utf8"
-): Promise<ArrayBuffer> => {
-  const bytes = getBytes(value, encoding);
-  return crypto.createHash("sha1").update(bytes).digest();
-};
-
-const SHA256 = async (
-  value: string | ArrayBuffer,
-  encoding: BufferEncoding = "utf8"
-): Promise<ArrayBuffer> => {
-  const bytes = getBytes(value, encoding);
-  return crypto.createHash("sha256").update(bytes).digest();
-};
-
-const SHA384 = async (
-  value: string | ArrayBuffer,
-  encoding: BufferEncoding = "utf8"
-): Promise<ArrayBuffer> => {
-  const bytes = getBytes(value, encoding);
-  return crypto.createHash("sha384").update(bytes).digest();
-};
-
-const SHA512 = async (
-  value: string | ArrayBuffer,
-  encoding: BufferEncoding = "utf8"
-): Promise<ArrayBuffer> => {
-  const bytes = getBytes(value, encoding);
-  return crypto.createHash("sha512").update(bytes).digest();
-};
-
-const MD5 = async (
-  value: string | ArrayBuffer,
-  encoding: BufferEncoding = "utf8"
-): Promise<ArrayBuffer> => {
-  const bytes = getBytes(value, encoding);
-  return md5.arrayBuffer(bytes);
-};
-
 const getBytes = (
   value: string | ArrayBuffer,
   encoding: BufferEncoding
@@ -166,33 +22,193 @@ const getBytes = (
   return bytes;
 };
 
-const verifyRSASignature = async (
-  publicKeyPEM: string,
-  data: ArrayBuffer,
-  signature: ArrayBuffer
-): Promise<boolean> => {
-  const verify = crypto.createVerify("SHA256");
-  verify.update(Buffer.from(data));
-  verify.end();
+class CryptoServiceNode implements ICryptoService {
+  public async encryptAESGCM(data: string): Promise<AESGCMOutput>;
+  public async encryptAESGCM(data: ArrayBuffer): Promise<AESGCMOutput>;
+  public async encryptAESGCM(
+    data: string | ArrayBuffer
+  ): Promise<AESGCMOutput> {
+    const key = crypto.randomBytes(32);
+    const iv = crypto.randomBytes(12);
+    const cipher = crypto.createCipheriv(AES_GCM_ALGO, key, iv);
+
+    let encrypted;
+    if (typeof data === "string") {
+      encrypted = cipher.update(data, "utf8");
+    } else {
+      encrypted = cipher.update(ensureUint8Array(data));
+    }
+    cipher.final();
+
+    const authTag = cipher.getAuthTag();
+    const encryptedWithTag = Buffer.concat([
+      Buffer.from(encrypted),
+      Buffer.from(authTag),
+    ]);
+
+    return {
+      key: key.buffer,
+      iv: iv.buffer,
+      data: encryptedWithTag,
+    };
+  }
 
-  const publicKey = crypto.createPublicKey(Buffer.from(publicKeyPEM, "utf-8"));
+  public async decryptAESGCM(
+    data: ArrayBuffer,
+    key: ArrayBuffer,
+    iv: ArrayBuffer
+  ): Promise<string>;
+  public async decryptAESGCM(
+    data: ArrayBuffer,
+    key: ArrayBuffer,
+    iv: ArrayBuffer,
+    returnBuffer: true
+  ): Promise<ArrayBuffer>;
+  public async decryptAESGCM(
+    data: ArrayBuffer,
+    key: ArrayBuffer,
+    iv: ArrayBuffer,
+    returnBuffer?: boolean
+  ): Promise<string | ArrayBuffer> {
+    const decipher = crypto.createDecipheriv(
+      AES_GCM_ALGO,
+      Buffer.from(key),
+      Buffer.from(iv)
+    );
+
+    const authTag = data.slice(data.byteLength - 16, data.byteLength);
+    const encrypted = data.slice(0, data.byteLength - 16);
+    if (returnBuffer) {
+      const decrypted: Buffer = decipher.update(ensureUint8Array(encrypted));
+      decipher.setAuthTag(Buffer.from(authTag));
+      return Buffer.concat([decrypted, decipher.final()]);
+    }
+
+    let str = decipher.update(ensureUint8Array(encrypted), null, "utf8");
+    decipher.setAuthTag(Buffer.from(authTag));
+    str += decipher.final("utf8");
+    return str;
+  }
 
-  return verify.verify(publicKey, Buffer.from(signature));
-};
+  public async generateRSAKeys(): Promise<RSAKeys> {
+    const { privateKey, publicKey } = crypto.generateKeyPairSync("rsa", {
+      modulusLength: 4096,
+      publicKeyEncoding: {
+        type: "spki",
+        format: "pem",
+      },
+      privateKeyEncoding: {
+        type: "pkcs8",
+        format: "pem",
+      },
+    });
+
+    return {
+      publicKeyPEM: publicKey,
+      privateKeyPEM: privateKey,
+    };
+  }
 
-const implementation: ICryptoService = {
-  encryptAESGCM,
-  decryptAESGCM,
-  generateRSAKeys,
-  encryptRSA,
-  decryptRSA,
-  SHA256,
-  signRSA,
-  verifyRSASignature,
-  SHA512,
-  SHA384,
-  SHA1,
-  MD5,
-};
+  public async encryptRSA(
+    publicKeyPEM: string,
+    data: ArrayBuffer
+  ): Promise<ArrayBuffer> {
+    const encryptedData = crypto.publicEncrypt(
+      {
+        key: publicKeyPEM,
+        padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
+        oaepHash: "sha256",
+      },
+      // We convert the data string to a buffer using `Buffer.from`
+      Buffer.from(data)
+    );
+
+    return encryptedData.buffer;
+  }
+
+  public async decryptRSA(
+    privateKeyPEM: string,
+    data: ArrayBuffer
+  ): Promise<ArrayBuffer> {
+    const encryptedData = crypto.privateDecrypt(
+      {
+        key: privateKeyPEM,
+        padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
+        oaepHash: "sha256",
+      },
+      // We convert the data string to a buffer using `Buffer.from`
+      Buffer.from(data)
+    );
+
+    return encryptedData.buffer;
+  }
+
+  public async signRSA(
+    privateKeyPEM: string,
+    data: ArrayBuffer
+  ): Promise<ArrayBuffer> {
+    const sign = crypto.createSign("SHA256");
+    sign.update(Buffer.from(data));
+    sign.end();
+
+    return sign.sign(privateKeyPEM).buffer;
+  }
+
+  public async SHA1(
+    value: string | ArrayBuffer,
+    encoding: BufferEncoding = "utf8"
+  ): Promise<ArrayBuffer> {
+    const bytes = getBytes(value, encoding);
+    return crypto.createHash("sha1").update(bytes).digest();
+  }
+
+  public async SHA256(
+    value: string | ArrayBuffer,
+    encoding: BufferEncoding = "utf8"
+  ): Promise<ArrayBuffer> {
+    const bytes = getBytes(value, encoding);
+    return crypto.createHash("sha256").update(bytes).digest();
+  }
+
+  public async SHA384(
+    value: string | ArrayBuffer,
+    encoding: BufferEncoding = "utf8"
+  ): Promise<ArrayBuffer> {
+    const bytes = getBytes(value, encoding);
+    return crypto.createHash("sha384").update(bytes).digest();
+  }
+
+  public async SHA512(
+    value: string | ArrayBuffer,
+    encoding: BufferEncoding = "utf8"
+  ): Promise<ArrayBuffer> {
+    const bytes = getBytes(value, encoding);
+    return crypto.createHash("sha512").update(bytes).digest();
+  }
+
+  public async MD5(
+    value: string | ArrayBuffer,
+    encoding: BufferEncoding = "utf8"
+  ): Promise<ArrayBuffer> {
+    const bytes = getBytes(value, encoding);
+    return md5.arrayBuffer(bytes);
+  }
+
+  public async verifyRSASignature(
+    publicKeyPEM: string,
+    data: ArrayBuffer,
+    signature: ArrayBuffer
+  ): Promise<boolean> {
+    const verify = crypto.createVerify("SHA256");
+    verify.update(Buffer.from(data));
+    verify.end();
+
+    const publicKey = crypto.createPublicKey(
+      Buffer.from(publicKeyPEM, "utf-8")
+    );
+
+    return verify.verify(publicKey, Buffer.from(signature));
+  }
+}
 
-export default implementation;
+export default CryptoServiceNode;
diff --git a/src/services/CryptoService/CryptoServiceWeb.ts b/src/services/CryptoService/CryptoServiceWeb.ts
index 1290a87759697548d5aebb6f7ee5b4feb9cfe3a9..d5ef7a08fee5c44b84b7df08b9c70016f4ac81a8 100644
--- a/src/services/CryptoService/CryptoServiceWeb.ts
+++ b/src/services/CryptoService/CryptoServiceWeb.ts
@@ -2,9 +2,9 @@ import md5 from "js-md5";
 import { AESGCMOutput, ICryptoService, RSAKeys } from "./ICryptoService";
 import { base64ToArrayBuffer } from "../../utils/common";
 
-async function exportKey(key: CryptoKey): Promise<ArrayBuffer> {
+const exportKey = async (key: CryptoKey): Promise<ArrayBuffer> => {
   return crypto.subtle.exportKey("raw", key);
-}
+};
 
 const convertPemToBinary = (pem: string) => {
   const lines = pem.split("\n");
@@ -23,141 +23,6 @@ const convertPemToBinary = (pem: string) => {
   return base64ToArrayBuffer(encoded);
 };
 
-const encryptRSA = async (
-  publicKeyPEM: string,
-  data: ArrayBuffer
-): Promise<ArrayBuffer> => {
-  const publicKey = await crypto.subtle.importKey(
-    "spki",
-    convertPemToBinary(publicKeyPEM),
-    {
-      name: "RSA-OAEP",
-      hash: "SHA-256",
-    },
-    true,
-    ["encrypt"]
-  );
-
-  return crypto.subtle.encrypt(
-    {
-      name: "RSA-OAEP",
-    },
-    publicKey,
-    data
-  );
-};
-
-export const verifyRSASignature = async (
-  publicKeyPEM: string,
-  data: ArrayBuffer,
-  signature: ArrayBuffer
-): Promise<boolean> => {
-  const publicKey = await crypto.subtle.importKey(
-    "spki",
-    convertPemToBinary(publicKeyPEM),
-    {
-      name: "RSASSA-PKCS1-v1_5",
-      hash: "SHA-256",
-    },
-    true,
-    ["verify"]
-  );
-
-  return await crypto.subtle.verify(
-    {
-      name: "RSASSA-PKCS1-v1_5",
-      hash: "SHA-256",
-    },
-    publicKey,
-    signature,
-    data
-  );
-};
-
-const encryptAESGCM = async (data: string): Promise<AESGCMOutput> => {
-  const key = await crypto.subtle.generateKey(
-    {
-      name: "AES-GCM",
-      length: 256,
-    },
-    true,
-    ["encrypt", "decrypt"]
-  );
-
-  const encoded = new TextEncoder().encode(data);
-  const iv = crypto.getRandomValues(new Buffer(12));
-  const encrypted = await crypto.subtle.encrypt(
-    { name: "AES-GCM", iv: iv },
-    key,
-    encoded
-  );
-  return { data: encrypted, key: await exportKey(key), iv };
-};
-
-const decryptAESGCM = async (
-  data: ArrayBuffer,
-  key: ArrayBuffer,
-  iv: ArrayBuffer
-): Promise<string> => {
-  const importedKey = await crypto.subtle.importKey(
-    "raw",
-    key,
-    {
-      name: "AES-GCM",
-      length: 256,
-    },
-    true,
-    ["encrypt", "decrypt"]
-  );
-
-  const decrypted = await crypto.subtle.decrypt(
-    { name: "AES-GCM", iv: iv },
-    importedKey,
-    data
-  );
-  return new TextDecoder().decode(decrypted);
-};
-
-const SHA1 = async (
-  value: string | ArrayBuffer,
-  encoding = "utf8"
-): Promise<ArrayBuffer> => {
-  const bytes = getBytes(value, encoding);
-  return await crypto.subtle.digest("SHA-1", bytes);
-};
-
-const SHA256 = async (
-  value: string | ArrayBuffer,
-  encoding = "utf8"
-): Promise<ArrayBuffer> => {
-  const bytes = getBytes(value, encoding);
-  return await crypto.subtle.digest("SHA-256", bytes);
-};
-
-const SHA384 = async (
-  value: string | ArrayBuffer,
-  encoding = "utf8"
-): Promise<ArrayBuffer> => {
-  const bytes = getBytes(value, encoding);
-  return await crypto.subtle.digest("SHA-384", bytes);
-};
-
-const SHA512 = async (
-  value: string | ArrayBuffer,
-  encoding = "utf8"
-): Promise<ArrayBuffer> => {
-  const bytes = getBytes(value, encoding);
-  return await crypto.subtle.digest("SHA-512", bytes);
-};
-
-const MD5 = async (
-  value: string | ArrayBuffer,
-  encoding = "utf8"
-): Promise<ArrayBuffer> => {
-  const bytes = getBytes(value, encoding);
-  return md5.arrayBuffer(bytes);
-};
-
 const getBytes = (value: string | ArrayBuffer, encoding): ArrayBuffer => {
   let bytes;
   if (typeof value === "string") {
@@ -173,25 +38,183 @@ const getBytes = (value: string | ArrayBuffer, encoding): ArrayBuffer => {
   return bytes;
 };
 
-const implementation: ICryptoService = {
-  encryptAESGCM,
-  decryptAESGCM,
-  verifyRSASignature,
-  generateRSAKeys(): Promise<RSAKeys> {
-    throw new Error("Surprise. Not implemented yet :)");
-  },
-  encryptRSA,
-  decryptRSA(privateKeyPEM: string, data: ArrayBuffer): Promise<ArrayBuffer> {
-    throw new Error("Surprise. Not implemented yet :)");
-  },
-  signRSA(privateKeyPEM: string, data: ArrayBuffer): Promise<ArrayBuffer> {
-    throw new Error("Surprise. Not implemented yet :)");
-  },
-  SHA1,
-  SHA384,
-  SHA256,
-  SHA512,
-  MD5,
-};
+class CryptoServiceWeb implements ICryptoService {
+  public async encryptAESGCM(data: string): Promise<AESGCMOutput>;
+  public async encryptAESGCM(data: ArrayBuffer): Promise<AESGCMOutput>;
+  public async encryptAESGCM(
+    data: string | ArrayBuffer
+  ): Promise<AESGCMOutput> {
+    const key = await crypto.subtle.generateKey(
+      {
+        name: "AES-GCM",
+        length: 256,
+      },
+      true,
+      ["encrypt", "decrypt"]
+    );
+
+    let encoded;
+    if (typeof data === "string") {
+      encoded = new TextEncoder().encode(data);
+    } else {
+      encoded = data;
+    }
+    const iv = crypto.getRandomValues(new Buffer(12));
+    const encrypted = await crypto.subtle.encrypt(
+      { name: "AES-GCM", iv: iv },
+      key,
+      encoded
+    );
+    return { data: encrypted, key: await exportKey(key), iv };
+  }
+
+  public async decryptAESGCM(
+    data: ArrayBuffer,
+    key: ArrayBuffer,
+    iv: ArrayBuffer
+  ): Promise<string>;
+  public async decryptAESGCM(
+    data: ArrayBuffer,
+    key: ArrayBuffer,
+    iv: ArrayBuffer,
+    returnBuffer: true
+  ): Promise<ArrayBuffer>;
+  public async decryptAESGCM(
+    data: ArrayBuffer,
+    key: ArrayBuffer,
+    iv: ArrayBuffer,
+    returnBuffer?: boolean
+  ): Promise<string | ArrayBuffer> {
+    const importedKey = await crypto.subtle.importKey(
+      "raw",
+      key,
+      {
+        name: "AES-GCM",
+        length: 256,
+      },
+      true,
+      ["encrypt", "decrypt"]
+    );
+
+    const decrypted = await crypto.subtle.decrypt(
+      { name: "AES-GCM", iv: iv },
+      importedKey,
+      data
+    );
+    if (returnBuffer) {
+      return decrypted;
+    }
+    return new TextDecoder().decode(decrypted);
+  }
+
+  public async verifyRSASignature(
+    publicKeyPEM: string,
+    data: ArrayBuffer,
+    signature: ArrayBuffer
+  ): Promise<boolean> {
+    const publicKey = await crypto.subtle.importKey(
+      "spki",
+      convertPemToBinary(publicKeyPEM),
+      {
+        name: "RSASSA-PKCS1-v1_5",
+        hash: "SHA-256",
+      },
+      true,
+      ["verify"]
+    );
+
+    return await crypto.subtle.verify(
+      {
+        name: "RSASSA-PKCS1-v1_5",
+        hash: "SHA-256",
+      },
+      publicKey,
+      signature,
+      data
+    );
+  }
+
+  public async generateRSAKeys(): Promise<RSAKeys> {
+    throw new Error("The function is not implemented");
+  }
+
+  public async encryptRSA(
+    publicKeyPEM: string,
+    data: ArrayBuffer
+  ): Promise<ArrayBuffer> {
+    const publicKey = await crypto.subtle.importKey(
+      "spki",
+      convertPemToBinary(publicKeyPEM),
+      {
+        name: "RSA-OAEP",
+        hash: "SHA-256",
+      },
+      true,
+      ["encrypt"]
+    );
+
+    return crypto.subtle.encrypt(
+      {
+        name: "RSA-OAEP",
+      },
+      publicKey,
+      data
+    );
+  }
+
+  public async decryptRSA(
+    privateKeyPEM: string,
+    data: ArrayBuffer
+  ): Promise<ArrayBuffer> {
+    throw new Error("The function is not implemented");
+  }
+
+  public async signRSA(
+    privateKeyPEM: string,
+    data: ArrayBuffer
+  ): Promise<ArrayBuffer> {
+    throw new Error("The function is not implemented");
+  }
+
+  public async SHA1(
+    value: string | ArrayBuffer,
+    encoding = "utf8"
+  ): Promise<ArrayBuffer> {
+    const bytes = getBytes(value, encoding);
+    return await crypto.subtle.digest("SHA-1", bytes);
+  }
+
+  public async SHA256(
+    value: string | ArrayBuffer,
+    encoding = "utf8"
+  ): Promise<ArrayBuffer> {
+    const bytes = getBytes(value, encoding);
+    return await crypto.subtle.digest("SHA-256", bytes);
+  }
+
+  public async SHA384(
+    value: string | ArrayBuffer,
+    encoding = "utf8"
+  ): Promise<ArrayBuffer> {
+    const bytes = getBytes(value, encoding);
+    return await crypto.subtle.digest("SHA-384", bytes);
+  }
+
+  public async SHA512(
+    value: string | ArrayBuffer,
+    encoding = "utf8"
+  ): Promise<ArrayBuffer> {
+    const bytes = getBytes(value, encoding);
+    return await crypto.subtle.digest("SHA-512", bytes);
+  }
+
+  public async MD5(
+    value: string | ArrayBuffer,
+    encoding = "utf8"
+  ): Promise<ArrayBuffer> {
+    const bytes = getBytes(value, encoding);
+    return md5.arrayBuffer(bytes);
+  }
+}
 
-export default implementation;
+export default CryptoServiceWeb;
diff --git a/src/services/CryptoService/ICryptoService.ts b/src/services/CryptoService/ICryptoService.ts
index a995e9770e35581e83a90291ea10b192767d2b35..0dfd66a86ab28a6a4fe1a2138a78628f18c20a06 100644
--- a/src/services/CryptoService/ICryptoService.ts
+++ b/src/services/CryptoService/ICryptoService.ts
@@ -10,12 +10,19 @@ export interface RSAKeys {
 }
 
 export interface ICryptoService {
-  encryptAESGCM: (data: string) => Promise<AESGCMOutput>;
-  decryptAESGCM: (
-    data: ArrayBuffer,
-    key: ArrayBuffer,
-    iv: ArrayBuffer
-  ) => Promise<string>;
+  encryptAESGCM: {
+    (data: string): Promise<AESGCMOutput>;
+    (data: ArrayBuffer): Promise<AESGCMOutput>;
+  };
+  decryptAESGCM: {
+    (data: ArrayBuffer, key: ArrayBuffer, iv: ArrayBuffer): Promise<string>;
+    (
+      data: ArrayBuffer,
+      key: ArrayBuffer,
+      iv: ArrayBuffer,
+      returnBuffer: true
+    ): Promise<ArrayBuffer>;
+  };
   generateRSAKeys: () => Promise<RSAKeys>;
   encryptRSA: (publicKeyPEM: string, data: ArrayBuffer) => Promise<ArrayBuffer>;
   decryptRSA: (
diff --git a/src/services/CryptoService/index.ts b/src/services/CryptoService/index.ts
index 601f403476c50ef6f5ddb1b5afe5d81437f1e07b..1ef2637ecf56b94f5c25ec53c219bb0af6732673 100644
--- a/src/services/CryptoService/index.ts
+++ b/src/services/CryptoService/index.ts
@@ -1,9 +1,10 @@
 import CryptoServiceNode from "./CryptoServiceNode";
 import CryptoServiceWeb from "./CryptoServiceWeb";
+import { ICryptoService } from "./ICryptoService";
 
-const service =
+const service: ICryptoService =
   typeof crypto !== "undefined" && crypto.subtle
-    ? CryptoServiceWeb
-    : CryptoServiceNode;
+    ? new CryptoServiceWeb()
+    : new CryptoServiceNode();
 
 export default service;
diff --git a/src/services/IPFSService.ts b/src/services/IPFSService.ts
new file mode 100644
index 0000000000000000000000000000000000000000..18a89c52087b4370cff1fe2792c7fe0eb9f548b1
--- /dev/null
+++ b/src/services/IPFSService.ts
@@ -0,0 +1,80 @@
+import axios from "axios";
+import { IpfsContentData, IpfsDataVersion } from "../types";
+import { base64ToArrayBuffer, decompressData } from "../utils/common";
+import QrCodeDataService from "./QrCodeDataService";
+import CryptoService from "./CryptoService";
+
+const GET_CONTENT_PATH = "/ipfs";
+
+class IPFSDataRetriever {
+  ipfsUrl;
+  constructor(ipfsUrl: string) {
+    this.ipfsUrl = ipfsUrl;
+  }
+
+  public async getDecodedContent(
+    contentData: IpfsContentData,
+    ipfsVersion: IpfsDataVersion
+  ): Promise<ArrayBuffer> {
+    const rawContent = await this.getRawContent(
+      contentData.cid,
+      contentData.head,
+      ipfsVersion
+    );
+    return this.decryptContent(rawContent, contentData.key);
+  }
+
+  private async getRawContent(
+    cid: string,
+    head: string,
+    ipfsVersion: IpfsDataVersion
+  ): Promise<ArrayBuffer> {
+    let assembledData;
+    if (cid) {
+      let ipfsFileContent;
+      if (ipfsVersion === undefined) {
+        const response = await axios({
+          url: `${this.ipfsUrl}${GET_CONTENT_PATH}/${cid}`,
+          method: "GET",
+        });
+        ipfsFileContent = base64ToArrayBuffer(response.data);
+      } else if (ipfsVersion === "v2") {
+        const result = await axios.get(
+          `${this.ipfsUrl}${GET_CONTENT_PATH}/${cid}`,
+          { responseType: "arraybuffer" }
+        );
+        ipfsFileContent = Buffer.from(result.data);
+      } else {
+        throw new Error("Unknown qrcode ipfs version");
+      }
+      assembledData = QrCodeDataService.assembleQrCodeData(
+        base64ToArrayBuffer(head),
+        ipfsFileContent
+      );
+    } else {
+      assembledData = base64ToArrayBuffer(head);
+    }
+    return assembledData;
+  }
+
+  private async decryptContent(
+    content: ArrayBuffer,
+    decryptionKey: string
+  ): Promise<ArrayBuffer> {
+    const { key, data: iv } =
+      QrCodeDataService.decodeKeyDataPair(decryptionKey);
+
+    const decryptedZip = await CryptoService.decryptAESGCM(
+      content,
+      base64ToArrayBuffer(key),
+      base64ToArrayBuffer(iv),
+      true
+    );
+
+    const decompressed = decompressData(decryptedZip);
+
+    return decompressed;
+  }
+}
+
+export default IPFSDataRetriever;
diff --git a/src/services/QrCodeDataService.ts b/src/services/QrCodeDataService.ts
index da633e1a9b7848c21356a376aed49758f0dc7dc2..03bc827ef1452d97d5059839e28c31167d668879 100644
--- a/src/services/QrCodeDataService.ts
+++ b/src/services/QrCodeDataService.ts
@@ -1,4 +1,3 @@
-import axios from "axios";
 import { KeyDataPair, MessageData } from "../types";
 import { vereign } from "../generated/qrcode_data_pb";
 
@@ -6,13 +5,8 @@ import {
   arrayBufferToBase64,
   base64ToArrayBuffer,
   ensureUint8Array,
-  decompressData,
-  arrayBufferToHex,
 } from "../utils/common";
 
-import CloudflareService from "./CloudflareService";
-import CryptoServiceDefault from "./CryptoService";
-import { ICryptoService } from "./CryptoService/ICryptoService";
 import { CryptoService } from "../index";
 
 const EmailDataMessageV1 = vereign.protobuf.qrcode_data.EmailData_V1;
@@ -38,9 +32,8 @@ const encodeEmailData = (emailData: MessageData): string => {
   };
 
   const wrappedDataMessage = WrapperDataMessage.fromObject(wrappedData);
-  const wrappedDataBuffer = WrapperDataMessage.encode(
-    wrappedDataMessage
-  ).finish();
+  const wrappedDataBuffer =
+    WrapperDataMessage.encode(wrappedDataMessage).finish();
   return arrayBufferToBase64(wrappedDataBuffer);
 };
 
@@ -112,62 +105,6 @@ const assembleQrCodeData = (
   return Buffer.concat([Buffer.from(head), Buffer.from(tail)]);
 };
 
-const computeQrCodeHash = async (emailData: MessageData): Promise<string> => {
-  let attachments = [];
-  if (emailData.attachments) {
-    attachments = emailData.attachments.map((attachment) => {
-      //TODO: check if we can use attachment hash, attachment hashAlg
-      if (attachment.url) {
-        return {
-          name: attachment.name,
-          size: attachment.size,
-          url: attachment.url,
-        };
-      }
-
-      return {
-        name: attachment.name,
-        size: attachment.size,
-      };
-    });
-  }
-
-  const dataForHashing = {
-    sender: emailData.sender,
-    subject: emailData.subject,
-    recipients: emailData.recipients,
-    attachments,
-  };
-
-  const promises = Object.values(dataForHashing).map((value) => {
-    const string = Buffer.from(
-      JSON.stringify(value, (key, value) =>
-        value instanceof Object && !(value instanceof Array)
-          ? Object.keys(value)
-              .sort()
-              .reduce((sorted, key) => {
-                sorted[key] = value[key];
-                return sorted;
-              }, {})
-          : value
-      )
-    ).toString();
-
-    return CryptoService.SHA256(string);
-  });
-
-  const hashArray = await Promise.all(promises);
-
-  const hashesAsAstring: string = hashArray
-    .map(arrayBufferToBase64)
-    .sort()
-    .join("\n");
-
-  const resultBuffer = await CryptoService.SHA256(hashesAsAstring);
-
-  return arrayBufferToBase64(resultBuffer);
-};
-
 const calculateQRCodeSignature = async (
   accountPrivateKey: string,
   qrCodeHash: string
@@ -196,90 +133,6 @@ const verifyQrCodeSignature = async (
   return result;
 };
 
-interface WithServices {
-  getMessageDataFromBase64: (base64: string) => Promise<MessageData>;
-}
-
-interface BackblazeDataPartRaw {
-  key_signature: string;
-  qr_code_data: string;
-  session_key: string;
-}
-
-/**
- * Higher order function to perform complex operations with the QR code data
- * @param cloudFlareServiceInstance = new CloudflareService("CDN_RUL", "DEFAULT_BUCKET")
- * @param landingPageServiceUrl - URL of the service responsible for RSA decryption
- * @param cryptoService - instance of ICryptoService. Override only needed cryptographic functions.
- * The rest will be filled with default ones from CryptoServiceWeb/CryptoServiceNode
- */
-const withServices = (
-  cloudFlareServiceInstance: CloudflareService,
-  landingPageServiceUrl: string,
-  cryptoService?: ICryptoService
-): WithServices => {
-  if (cryptoService) {
-    cryptoService = { ...CryptoServiceDefault, ...cryptoService };
-  } else {
-    cryptoService = CryptoServiceDefault;
-  }
-
-  return {
-    getMessageDataFromBase64: async (base64: string) => {
-      const qrCodeDataPart = decodeKeyDataPair(base64);
-
-      const base64SHA256 = await cryptoService.SHA256(base64);
-
-      const {
-        data: backblazeDataPart,
-      } = await cloudFlareServiceInstance.fetchFile<BackblazeDataPartRaw>(
-        `qrcode-${arrayBufferToHex(base64SHA256)}`
-      );
-
-      const assembledData = assembleQrCodeData(
-        base64ToArrayBuffer(qrCodeDataPart.data),
-        base64ToArrayBuffer(backblazeDataPart.qr_code_data)
-      );
-
-      const {
-        key: aesEncryptedSessionKey,
-        data: storageIv,
-      } = decodeKeyDataPair(backblazeDataPart.session_key);
-
-      const rsaEncryptedSessionKey = await cryptoService.decryptAESGCM(
-        base64ToArrayBuffer(aesEncryptedSessionKey),
-        base64ToArrayBuffer(qrCodeDataPart.key),
-        base64ToArrayBuffer(storageIv)
-      );
-
-      const {
-        data: { key: encodedSessionKey },
-      } = await axios({
-        url: `${landingPageServiceUrl}/api/hsm/decrypt`,
-        method: "POST",
-        headers: {
-          "Content-Type": "application/json",
-        },
-        data: {
-          key_signature: backblazeDataPart.key_signature,
-          encrypted_key: rsaEncryptedSessionKey,
-        },
-      });
-
-      const decodedSessionKey = decodeKeyDataPair(encodedSessionKey);
-
-      const decryptedEmailData = await cryptoService.decryptAESGCM(
-        assembledData,
-        base64ToArrayBuffer(decodedSessionKey.key),
-        base64ToArrayBuffer(decodedSessionKey.data)
-      );
-
-      const decompressedEmailData = decompressData(decryptedEmailData);
-      return decodeEmailData(decompressedEmailData);
-    },
-  };
-};
-
 export default {
   encodeEmailData,
   decodeEmailData,
@@ -287,8 +140,6 @@ export default {
   decodeKeyDataPair,
   breakQrCodeData,
   assembleQrCodeData,
-  computeQrCodeHash,
   calculateQRCodeSignature,
   verifyQrCodeSignature,
-  withServices,
 };
diff --git a/src/types.ts b/src/types.ts
index 89bf1b6fa821efb086c67201a44f08120d71d9ae..be29d595d9ee722535f70fa3d72f2ef996aa8e6b 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -25,9 +25,13 @@ export interface IpfsAttachmentData {
   key: string;
   contentHash: string;
   head: string;
+  contentType: string;
 }
 
+export type IpfsDataVersion = undefined | "v2";
+
 export interface IpfsData {
+  version: IpfsDataVersion;
   plaintText: IpfsContentData;
   html: IpfsContentData;
   attachments: Array<IpfsAttachmentData>;
diff --git a/vereign/protobuf/qrcode_data.proto b/vereign/protobuf/qrcode_data.proto
index 8d9c61e7947ed6b1116cb454d91f58917dc3adbf..5e4e07ea749162a28d3b14ffbf75ceb40f5627ae 100644
--- a/vereign/protobuf/qrcode_data.proto
+++ b/vereign/protobuf/qrcode_data.proto
@@ -33,12 +33,14 @@ message IpfsAttachmentData_V1 {
     string key = 2;
     string contentHash = 3;
     string head = 4;
+    string contentType = 5;
 }
 
 message IpfsData_V1 {
   IpfsContentData_V1 plaintText = 1;
   IpfsContentData_V1 html = 2;
   repeated IpfsAttachmentData_V1 attachments = 3;
+  string version = 4;
 }
 
 message EmailData_V1 {