import { Injectable } from "@angular/core"; import { TwitterApiProvider } from "../twitter-api/twitter-api"; import { P2pStorageIpfsProvider } from "../p2p-storage-ipfs/p2p-storage-ipfs"; import { Storage } from "@ionic/storage"; import NodeRSA from "node-rsa"; declare var TextDecoder: any; declare var TextEncoder: any; @Injectable() export class CryptoProvider { ownUserId: string; IV_LENGTH = 12; HYBRID_OSN_AES_KEY = "Z1vxAULQnZdoWhJOvv+hWEvVpyUHzNjD/ichEE2c8i4="; constructor( private twitter: TwitterApiProvider, private ipfs: P2pStorageIpfsProvider, private storage: Storage ) { this.init(); } private async init() { this.ownUserId = await this.storage.get("userId"); } /** * Publishs the public key history with the latest key * @param key key to publish */ public async publishPublicKey(key: string) { let publicKeyHistory = await this.getKeyHistory(this.ownUserId); // Todo: avoid publishing the same public key twice - check if new key equals newest key in history // Add new key to history const newKey = { key: key, validFrom: Date.now() }; if (publicKeyHistory) { publicKeyHistory["keys"].push(newKey); } else { publicKeyHistory = { keys: [newKey] }; } // Ecnrypt key history const encryptedPublicKeyHistory = await this.aesEncrypt( JSON.stringify(publicKeyHistory) ); // Publish updated key history... const res = await this.ipfs.storePublicKey(encryptedPublicKeyHistory); // tweet ipfs link const tweetResponse = await this.twitter.tweet( "ipfs://" + res["Hash"] + " #hybridOSN" ); // ... and update description in user profile with tweet id this.twitter.updateProfileDescription( "tweet://" + tweetResponse["data"]["id_str"] + " #hybridOSN" ); } private async getKeyHistory(userId: string) { // Get user description const userData = await this.twitter.fetchUser(userId); const profileDescription = userData["description"]; // Get tweet with link to key history const tweetId = this.extractTweetId(profileDescription); if (tweetId.length === 0) { return null; } const tweetWithKeyHistoryLink = await this.twitter.fetchTweet(tweetId); // Extract link to public key const link = this.extractLinkFromDescription( tweetWithKeyHistoryLink["data"]["full_text"] ); // Fetch public key history if (link.length) { const encryptedKeyHistory = await this.ipfs.fetchJson(link); // Decrypt key history const keyHistory = await this.aesDecrypt(encryptedKeyHistory.toString()); return JSON.parse(keyHistory); } else { return null; } } private extractTweetId(text: string): string { for (let word of text.split(" ")) { if (this.isTweetId(word)) { return word.substr(8); } } return ""; } private extractLinkFromDescription(text: string): string { for (let word of text.split(" ")) { if (this.isIpfsLink(word)) { return word.substr(7); } } return ""; } private isIpfsLink(word: string): boolean { return /ipfs:\/\/Qm[a-zA-Z0-9]+/.test(word); } private isTweetId(word: string): boolean { return /tweet:\/\/[0-9]+/.test(word); } /** * Generates a RSA key pair object */ public async generateRsaKeys() { return await crypto.subtle.generateKey( { name: "RSA-OAEP", modulusLength: 1024, publicExponent: new Uint8Array([0x01, 0x00, 0x01]), hash: { name: "SHA-256" } }, true, ["encrypt", "decrypt"] ); } /** * extracts the private key from the key object and transforms it to readable text * @param keys key object */ public async extractPrivateKey(keys): Promise { return btoa( String.fromCharCode.apply( null, new Uint8Array(await crypto.subtle.exportKey("pkcs8", keys.privateKey)) ) ); } /** * extracts the public key from the key object and transforms it to readable text * @param keys key object */ public async extractPublicKey(keys): Promise { return btoa( String.fromCharCode.apply( null, new Uint8Array(await crypto.subtle.exportKey("spki", keys.publicKey)) ) ); } /** * checks if the latest published key is the same as the one saved in app settings */ public async isPublicKeyPublished(): Promise { const publicKey = await this.storage.get("publicKey"); const keyHistory = await this.getKeyHistory(this.ownUserId); if (keyHistory && publicKey.length) { const newestPublicKey = keyHistory["keys"].reverse()[0]["key"]; return newestPublicKey === publicKey; } else { return false; } } /** * checks if a private key is already set */ public async isPrivateKeySet(): Promise { const privateKey = await this.storage.get("privateKey"); return privateKey; } /** * Encrypt text with RSA * @param plainText plain text * @param privateKey private key */ public encrypt(plainText: string, privateKey: string) { const key = new NodeRSA( `-----BEGIN PRIVATE KEY-----${privateKey}-----END PRIVATE KEY-----` ); return key.encryptPrivate(plainText, "base64"); } /** * Decrypt secret with RSA * @param encryptedMessage encrypted message * @param publicKey public key */ public decrypt(encryptedMessage: string, publicKey: string): string { const key = new NodeRSA( `-----BEGIN PUBLIC KEY-----${publicKey}-----END PUBLIC KEY-----` ); return key.decryptPublic(encryptedMessage).toString(); } /** * Fetches the public key history for a given user id * @param userId user id */ public async fetchPublicKeyHistoryForUser(userId: string): Promise { const keyHistory = await this.getKeyHistory(userId); return keyHistory["keys"].reverse(); } private async aesEncrypt(plainText: string) { const utf8BytesOfPlainText = new TextEncoder().encode(plainText); const iv = crypto.getRandomValues(new Uint8Array(this.IV_LENGTH)); const keyBytes = this.base64ToBytes(this.HYBRID_OSN_AES_KEY); const algorithm = { name: "AES-GCM", iv, length: 256 }; return crypto.subtle .importKey("raw", keyBytes, algorithm, false, ["encrypt"]) .then((cryptoKey: CryptoKey) => { return crypto.subtle.encrypt( algorithm, cryptoKey, utf8BytesOfPlainText ); }) .then((encData: ArrayBuffer) => this.mergeBytes(iv, this.bufferToBytes(encData)) ) .then(this.bytesToBase64); } private aesDecrypt(encryptedMessage: string) { try { const ivWithEncryptedBytes = this.base64ToBytes(encryptedMessage); const { encryptedBytes, ivBytes } = this.splitIvAndEncrypted( ivWithEncryptedBytes ); const keyBytes = this.base64ToBytes(this.HYBRID_OSN_AES_KEY); const algorithm = { name: "AES-GCM", iv: ivBytes, length: 256 }; return crypto.subtle .importKey("raw", keyBytes, algorithm, false, ["decrypt"]) .then((cryptoKey: CryptoKey) => { return crypto.subtle.decrypt(algorithm, cryptoKey, encryptedBytes); }) .then((decryptedData: ArrayBuffer) => { return new TextDecoder().decode(decryptedData); }); } catch (e) { return Promise.reject(e); } } private bufferToBytes(arrayBuffer: ArrayBuffer) { return new Uint8Array(arrayBuffer); } private bytesToBase64(bytes: Uint8Array): string { let binary = ""; const len = bytes.length; for (let i = 0; i < len; i++) { binary += String.fromCharCode(bytes[i]); } return btoa(binary); } private base64ToBytes(base64: string): Uint8Array { const binaryString = atob(base64); const len = binaryString.length; const bytes = new Uint8Array(len); for (let i = 0; i < len; i++) { bytes[i] = binaryString.charCodeAt(i); } return bytes; } private mergeBytes(a: Uint8Array, b: Uint8Array) { const aLen = a.length; const bLen = b.length; const res = new Uint8Array(aLen + bLen); for (let i = 0; i < aLen; i++) { res[i] = a[i]; } for (let i = 0; i < bLen; i++) { res[i + aLen] = b[i]; } return res; } private splitIvAndEncrypted(ivWithEncryptedBytes: Uint8Array) { const dataLen = ivWithEncryptedBytes.length; const ivBytes = new Uint8Array(this.IV_LENGTH); const encryptedBytes = new Uint8Array(dataLen - this.IV_LENGTH); for (let i = 0; i < this.IV_LENGTH; i++) { ivBytes[i] = ivWithEncryptedBytes[i]; } for (let i = this.IV_LENGTH; i < dataLen; i++) { encryptedBytes[i - this.IV_LENGTH] = ivWithEncryptedBytes[i]; } return { ivBytes, encryptedBytes }; } }