diff --git a/.gitignore b/.gitignore
index 6c8b99c856..d0ae0b8085 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
 /.config
 /.vscode
 /node_modules
+/build
 /built
 /data
 npm-debug.log
diff --git a/binding.gyp b/binding.gyp
new file mode 100644
index 0000000000..0349526d52
--- /dev/null
+++ b/binding.gyp
@@ -0,0 +1,9 @@
+{
+	'targets': [
+		{
+			'target_name': 'crypto_key',
+			'sources': ['src/crypto_key.cc'],
+			'include_dirs': ['<!(node -e "require(\'nan\')")']
+		}
+	]
+}
diff --git a/gulpfile.ts b/gulpfile.ts
index c10d0a98d0..9c61e3a1cb 100644
--- a/gulpfile.ts
+++ b/gulpfile.ts
@@ -69,6 +69,7 @@ gulp.task('build:ts', () => {
 
 gulp.task('build:copy', () =>
 	gulp.src([
+		'./build/Release/crypto_key.node',
 		'./src/**/assets/**/*',
 		'!./src/web/app/**/assets/**/*'
 	]).pipe(gulp.dest('./built/'))
diff --git a/package.json b/package.json
index 3ec1620dd5..eee658fbd9 100644
--- a/package.json
+++ b/package.json
@@ -145,6 +145,7 @@
 		"morgan": "1.9.0",
 		"ms": "2.1.1",
 		"multer": "1.3.0",
+		"nan": "^2.10.0",
 		"node-sass": "4.7.2",
 		"node-sass-json-importer": "3.1.5",
 		"nprogress": "0.2.0",
diff --git a/src/api/models/user.ts b/src/api/models/user.ts
index 3f0b10d4c2..d3875a65b9 100644
--- a/src/api/models/user.ts
+++ b/src/api/models/user.ts
@@ -59,6 +59,7 @@ export type IUser = {
 	is_suspended: boolean;
 	keywords: string[];
 	account: {
+		keypair: string;
 		email: string;
 		links: string[];
 		password: string;
@@ -160,6 +161,7 @@ export const pack = (
 	delete _user.latest_post;
 
 	// Remove private properties
+	delete _user.account.keypair;
 	delete _user.account.password;
 	delete _user.account.token;
 	delete _user.account.two_factor_temp_secret;
diff --git a/src/api/private/signup.ts b/src/api/private/signup.ts
index 902642425c..690f3001cc 100644
--- a/src/api/private/signup.ts
+++ b/src/api/private/signup.ts
@@ -1,6 +1,7 @@
 import * as uuid from 'uuid';
 import * as express from 'express';
 import * as bcrypt from 'bcryptjs';
+import { generate as generateKeypair } from '../../crypto_key';
 import recaptcha = require('recaptcha-promise');
 import User, { IUser, validateUsername, validatePassword, pack } from '../models/user';
 import generateUserToken from '../common/generate-native-user-token';
@@ -119,6 +120,7 @@ export default async (req: express.Request, res: express.Response) => {
 		username: username,
 		username_lower: username.toLowerCase(),
 		account: {
+			keypair: generateKeypair(),
 			token: secret,
 			email: null,
 			links: null,
diff --git a/src/crypto_key.cc b/src/crypto_key.cc
new file mode 100644
index 0000000000..c8e4d8f7f0
--- /dev/null
+++ b/src/crypto_key.cc
@@ -0,0 +1,111 @@
+#include <nan.h>
+#include <openssl/bio.h>
+#include <openssl/buffer.h>
+#include <openssl/crypto.h>
+#include <openssl/pem.h>
+#include <openssl/rsa.h>
+#include <openssl/x509.h>
+
+NAN_METHOD(extractPublic)
+{
+	const auto sourceString = info[0]->ToString();
+	if (!sourceString->IsOneByte()) {
+		Nan::ThrowError("Malformed character found");
+		return;
+	}
+
+	size_t sourceLength = sourceString->Length();
+	const auto sourceBuf = new char[sourceLength];
+
+	Nan::DecodeWrite(sourceBuf, sourceLength, sourceString);
+
+	const auto source = BIO_new_mem_buf(sourceBuf, sourceLength);
+	if (source == nullptr) {
+		Nan::ThrowError("Memory allocation failed");
+		delete sourceBuf;
+		return;
+	}
+
+	const auto rsa = PEM_read_bio_RSAPrivateKey(source, nullptr, nullptr, nullptr);
+
+	BIO_free(source);
+	delete sourceBuf;
+
+	if (rsa == nullptr) {
+		Nan::ThrowError("Decode failed");
+		return;
+	}
+
+	const auto destination = BIO_new(BIO_s_mem());
+	if (destination == nullptr) {
+		Nan::ThrowError("Memory allocation failed");
+		return;
+	}
+
+	const auto result = PEM_write_bio_RSAPublicKey(destination, rsa);
+
+	RSA_free(rsa);
+
+	if (result != 1) {
+		Nan::ThrowError("Public key extraction failed");
+		BIO_free(destination);
+		return;
+	}
+
+	char *pem;
+	const auto pemLength = BIO_get_mem_data(destination, &pem);
+
+	info.GetReturnValue().Set(Nan::Encode(pem, pemLength));
+	BIO_free(destination);
+}
+
+NAN_METHOD(generate)
+{
+	const auto exponent = BN_new();
+	const auto mem = BIO_new(BIO_s_mem());
+	const auto rsa = RSA_new();
+	char *data;
+	long result;
+
+	if (exponent == nullptr || mem == nullptr || rsa == nullptr) {
+		Nan::ThrowError("Memory allocation failed");
+		goto done;
+	}
+
+	result = BN_set_word(exponent, 65537);
+	if (result != 1) {
+		Nan::ThrowError("Exponent setting failed");
+		goto done;
+	}
+
+	result = RSA_generate_key_ex(rsa, 2048, exponent, nullptr);
+	if (result != 1) {
+		Nan::ThrowError("Key generation failed");
+		goto done;
+	}
+
+	result = PEM_write_bio_RSAPrivateKey(mem, rsa, NULL, NULL, 0, NULL, NULL);
+	if (result != 1) {
+		Nan::ThrowError("Key export failed");
+		goto done;
+	}
+
+	result = BIO_get_mem_data(mem, &data);
+	info.GetReturnValue().Set(Nan::Encode(data, result));
+
+done:
+	RSA_free(rsa);
+	BIO_free(mem);
+	BN_free(exponent);
+}
+
+NAN_MODULE_INIT(InitAll)
+{
+	Nan::Set(target, Nan::New<v8::String>("extractPublic").ToLocalChecked(),
+		Nan::GetFunction(Nan::New<v8::FunctionTemplate>(extractPublic)).ToLocalChecked());
+
+	Nan::Set(target, Nan::New<v8::String>("generate").ToLocalChecked(),
+		Nan::GetFunction(Nan::New<v8::FunctionTemplate>(generate)).ToLocalChecked());
+}
+
+NODE_MODULE(crypto_key, InitAll);
diff --git a/src/crypto_key.d.ts b/src/crypto_key.d.ts
new file mode 100644
index 0000000000..28ac2f9683
--- /dev/null
+++ b/src/crypto_key.d.ts
@@ -0,0 +1 @@
+export function generate(): String;
diff --git a/src/web/app/common/views/components/media-list.vue b/src/web/app/common/views/components/media-list.vue
index d0da584a40..64172ad0b4 100644
--- a/src/web/app/common/views/components/media-list.vue
+++ b/src/web/app/common/views/components/media-list.vue
@@ -1,7 +1,8 @@
 <template>
 <div class="mk-media-list" :data-count="mediaList.length">
 	<template v-for="media in mediaList">
-		<mk-media-image :image="media" :key="media.id"/>
+		<mk-media-video :video="media" :key="media.id" v-if="media.type.startsWith('video')" :inline-playable="mediaList.length === 1"/>
+		<mk-media-image :image="media" :key="media.id" v-else />
 	</template>
 </div>
 </template>
diff --git a/src/web/app/desktop/views/components/index.ts b/src/web/app/desktop/views/components/index.ts
index 9bca603a53..3798bf6d2d 100644
--- a/src/web/app/desktop/views/components/index.ts
+++ b/src/web/app/desktop/views/components/index.ts
@@ -13,6 +13,7 @@ import analogClock from './analog-clock.vue';
 import ellipsisIcon from './ellipsis-icon.vue';
 import mediaImage from './media-image.vue';
 import mediaImageDialog from './media-image-dialog.vue';
+import mediaVideo from './media-video.vue';
 import notifications from './notifications.vue';
 import postForm from './post-form.vue';
 import repostForm from './repost-form.vue';
@@ -42,6 +43,7 @@ Vue.component('mk-analog-clock', analogClock);
 Vue.component('mk-ellipsis-icon', ellipsisIcon);
 Vue.component('mk-media-image', mediaImage);
 Vue.component('mk-media-image-dialog', mediaImageDialog);
+Vue.component('mk-media-video', mediaVideo);
 Vue.component('mk-notifications', notifications);
 Vue.component('mk-post-form', postForm);
 Vue.component('mk-repost-form', repostForm);
diff --git a/src/web/app/desktop/views/components/media-video-dialog.vue b/src/web/app/desktop/views/components/media-video-dialog.vue
new file mode 100644
index 0000000000..cbf862cd1c
--- /dev/null
+++ b/src/web/app/desktop/views/components/media-video-dialog.vue
@@ -0,0 +1,70 @@
+<template>
+<div class="mk-media-video-dialog">
+	<div class="bg" @click="close"></div>
+	<video :src="video.url" :title="video.name" controls autoplay ref="video"/>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import * as anime from 'animejs';
+
+export default Vue.extend({
+	props: ['video', 'start'],
+	mounted() {
+		anime({
+			targets: this.$el,
+			opacity: 1,
+			duration: 100,
+			easing: 'linear'
+		});
+		const videoTag = this.$refs.video as HTMLVideoElement
+		if (this.start) videoTag.currentTime = this.start
+	},
+	methods: {
+		close() {
+			anime({
+				targets: this.$el,
+				opacity: 0,
+				duration: 100,
+				easing: 'linear',
+				complete: () => this.$destroy()
+			});
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-media-video-dialog
+	display block
+	position fixed
+	z-index 2048
+	top 0
+	left 0
+	width 100%
+	height 100%
+	opacity 0
+
+	> .bg
+		display block
+		position fixed
+		z-index 1
+		top 0
+		left 0
+		width 100%
+		height 100%
+		background rgba(0, 0, 0, 0.7)
+
+	> video
+		position fixed
+		z-index 2
+		top 0
+		right 0
+		bottom 0
+		left 0
+		max-width 80vw
+		max-height 80vh
+		margin auto
+
+</style>
diff --git a/src/web/app/desktop/views/components/media-video.vue b/src/web/app/desktop/views/components/media-video.vue
new file mode 100644
index 0000000000..4fd955a821
--- /dev/null
+++ b/src/web/app/desktop/views/components/media-video.vue
@@ -0,0 +1,67 @@
+<template>
+	<video class="mk-media-video"
+		:src="video.url"
+		:title="video.name"
+		controls
+		@dblclick.prevent="onClick"
+		ref="video"
+		v-if="inlinePlayable" />
+	<a class="mk-media-video-thumbnail"
+		:href="video.url"
+		:style="imageStyle"
+		@click.prevent="onClick"
+		:title="video.name"
+		v-else>
+		%fa:R play-circle%
+	</a>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import MkMediaVideoDialog from './media-video-dialog.vue';
+
+export default Vue.extend({
+	props: ['video', 'inlinePlayable'],
+	computed: {
+		imageStyle(): any {
+			return {
+				'background-image': `url(${this.video.url}?thumbnail&size=512)`
+			};
+		}
+	},
+	methods: {
+		onClick() {
+			const videoTag = this.$refs.video as (HTMLVideoElement | null)
+			var start = 0
+			if (videoTag) {
+				start = videoTag.currentTime
+				videoTag.pause()
+			}
+			(this as any).os.new(MkMediaVideoDialog, {
+				video: this.video,
+				start,
+			})
+		}
+	}
+})
+</script>
+
+<style lang="stylus" scoped>
+.mk-media-video
+	display block
+	width 100%
+	height 100%
+	border-radius 4px
+.mk-media-video-thumbnail
+	display flex
+	justify-content center
+	align-items center
+	font-size 3.5em
+
+	cursor zoom-in
+	overflow hidden
+	background-position center
+	background-size cover
+	width 100%
+	height 100%
+</style>
diff --git a/src/web/app/mobile/views/components/index.ts b/src/web/app/mobile/views/components/index.ts
index 4743f50e0d..fb8f65f47d 100644
--- a/src/web/app/mobile/views/components/index.ts
+++ b/src/web/app/mobile/views/components/index.ts
@@ -5,6 +5,7 @@ import timeline from './timeline.vue';
 import post from './post.vue';
 import posts from './posts.vue';
 import mediaImage from './media-image.vue';
+import mediaVideo from './media-video.vue';
 import drive from './drive.vue';
 import postPreview from './post-preview.vue';
 import subPostContent from './sub-post-content.vue';
@@ -27,6 +28,7 @@ Vue.component('mk-timeline', timeline);
 Vue.component('mk-post', post);
 Vue.component('mk-posts', posts);
 Vue.component('mk-media-image', mediaImage);
+Vue.component('mk-media-video', mediaVideo);
 Vue.component('mk-drive', drive);
 Vue.component('mk-post-preview', postPreview);
 Vue.component('mk-sub-post-content', subPostContent);
diff --git a/src/web/app/mobile/views/components/media-video.vue b/src/web/app/mobile/views/components/media-video.vue
new file mode 100644
index 0000000000..68cd48587a
--- /dev/null
+++ b/src/web/app/mobile/views/components/media-video.vue
@@ -0,0 +1,36 @@
+<template>
+	<a class="mk-media-video"
+		:href="video.url"
+		target="_blank"
+		:style="imageStyle"
+		:title="video.name">
+		%fa:R play-circle%
+	</a>
+</template>
+
+<script lang="ts">
+import Vue from 'vue'
+export default Vue.extend({
+	props: ['video'],
+	computed: {
+		imageStyle(): any {
+			return {
+				'background-image': `url(${this.video.url}?thumbnail&size=512)`
+			};
+		}
+	},})
+</script>
+
+<style lang="stylus" scoped>
+.mk-media-video
+	display flex
+	justify-content center
+	align-items center
+
+	font-size 3.5em
+	overflow hidden
+	background-position center
+	background-size cover
+	width 100%
+	height 100%
+</style>
diff --git a/test/api.js b/test/api.js
index 9e55dd991e..b8b2aecc99 100644
--- a/test/api.js
+++ b/test/api.js
@@ -1161,6 +1161,7 @@ function insertSakurako(opts) {
 		username: 'sakurako',
 		username_lower: 'sakurako',
 		account: {
+			keypair: '-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAtdTG9rlFWjNqhgbg2V6X5XF1WpQXZS3KNXykEWl2UAiMyfVV\nBvf3zQP0dDEdNtcqdPJgis03bpiHCzQusc/YLyHYB0m+TJXsxJatb8cqUogOFeE4\ngQ4Dc5kAT6gLh/d4yz03EIg9bizX07EiGWnZqWxb+21ypqsPxST64sAtG9f5O/G4\nXe2m3cSbfAAvEUP1Ig1LUNyJB4jhM60w1cQic/qO8++sk/+GoX9g71X+i4NArGv+\n1c11acDIIPGAAQpFeYVeGaKakNDNp8RtJJp8R8FLwJXZ4/gATBnScCiHUSrGfRly\nYyR0w/BNlQ6/NijAdB9pR5csPvyIPkx1gauZewIDAQABAoIBAQCwWf/mhuY2h6uG\n9eDZsZ7Mj2/sO7k9Dl4R5iMSKCDxmnlB3slqitExa+aJUqEs8R5icjkkJcjfYNuJ\nCEFJf3YCsGZfGyyQBtCuEh2ATcBEb2SJ3/f3YuoCEaB1oVwdsOzc4TAovpol4yQo\nUqHp1/mdElVb01jhQQN4h1c02IJnfzvfU1C8szBni+Etfd+MxqGfv006DY3KOEb3\nlCrCS3GmooJW2Fjj7q1kCcaEQbMB1/aQHLXd1qe3KJOzXh3Voxsp/jEH0hvp2TII\nfY9UK+b7mA+xlvXwKuTkHVaZm0ylg0nbembS8MF4GfFMujinSexvLrVKaQhdMFoF\nvBLxHYHRAoGBANfNVYJYeCDPFNLmak5Xg33Rfvc2II8UmrZOVdhOWs8ZK0pis9e+\nPo2MKtTzrzipXI2QXv5w7kO+LJWNDva+xRlW8Wlj9Dde9QdQ7Y8+dk7SJgf24DzM\n023elgX5DvTeLODjStk6SMPRL0FmGovUqAAA8ZeHtJzkIr1HROWnQiwnAoGBANez\nhFwKnVoQu0RpBz/i4W0RKIxOwltN2zmlN8KjJPhSy00A7nBUfKLRbcwiSHE98Yi/\nUrXwMwR5QeD2ngngRppddJnpiRfjNjnsaqeqNtpO8AxB3XjpCC5zmHUMFHKvPpDj\n1zU/F44li0YjKcMBebZy9PbfAjrIgJfxhPo/oXiNAoGAfx6gaTjOAp2ZaaZ7Jozc\nkyft/5et1DrR6+P3I4T8bxQncRj1UXfqhxzzOiAVrm3tbCKIIp/JarRCtRGzp9u2\nZPfXGzra6CcSdW3Rkli7/jBCYNynOIl7XjQI8ZnFmq6phwu80ntH07mMeZy4tHff\nQqlLpvQ0i1rDr/Wkexdsnm8CgYBgxha9ILoF/Xm3MJPjEsxmnYsen/tM8XpIu5pv\nxbhBfQvfKWrQlOcyOVnUexEbVVo3KvdVz0VkXW60GpE/BxNGEGXO49rxD6x1gl87\nh/+CJGZIaYiOxaY5CP2+jcPizEL6yG32Yq8TxD5fIkmLRu8vbxX+aIFclDY1dVNe\n3wt3xQKBgGEL0EjwRch+P2V+YHAhbETPrEqJjHRWT95pIdF9XtC8fasSOVH81cLX\nXXsX1FTvOJNwG9Nk8rQjYJXGTb2O/2unaazlYUwxKwVpwuGzz/vhH/roHZBAkIVT\njvpykpn9QMezEdpzj5BEv01QzSYBPzIh5myrpoJIoSW7py7zFG3h\n-----END RSA PRIVATE KEY-----\n',
 			token: '!00000000000000000000000000000000',
 			password: '$2a$08$FnHXg3tP.M/kINWgQSXNqeoBsiVrkj.ecXX8mW9rfBzMRkibYfjYy', // HimawariDaisuki06160907
 			profile: {},
@@ -1175,6 +1176,7 @@ function insertHimawari(opts) {
 		username: 'himawari',
 		username_lower: 'himawari',
 		account: {
+			keypair: '-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAtdTG9rlFWjNqhgbg2V6X5XF1WpQXZS3KNXykEWl2UAiMyfVV\nBvf3zQP0dDEdNtcqdPJgis03bpiHCzQusc/YLyHYB0m+TJXsxJatb8cqUogOFeE4\ngQ4Dc5kAT6gLh/d4yz03EIg9bizX07EiGWnZqWxb+21ypqsPxST64sAtG9f5O/G4\nXe2m3cSbfAAvEUP1Ig1LUNyJB4jhM60w1cQic/qO8++sk/+GoX9g71X+i4NArGv+\n1c11acDIIPGAAQpFeYVeGaKakNDNp8RtJJp8R8FLwJXZ4/gATBnScCiHUSrGfRly\nYyR0w/BNlQ6/NijAdB9pR5csPvyIPkx1gauZewIDAQABAoIBAQCwWf/mhuY2h6uG\n9eDZsZ7Mj2/sO7k9Dl4R5iMSKCDxmnlB3slqitExa+aJUqEs8R5icjkkJcjfYNuJ\nCEFJf3YCsGZfGyyQBtCuEh2ATcBEb2SJ3/f3YuoCEaB1oVwdsOzc4TAovpol4yQo\nUqHp1/mdElVb01jhQQN4h1c02IJnfzvfU1C8szBni+Etfd+MxqGfv006DY3KOEb3\nlCrCS3GmooJW2Fjj7q1kCcaEQbMB1/aQHLXd1qe3KJOzXh3Voxsp/jEH0hvp2TII\nfY9UK+b7mA+xlvXwKuTkHVaZm0ylg0nbembS8MF4GfFMujinSexvLrVKaQhdMFoF\nvBLxHYHRAoGBANfNVYJYeCDPFNLmak5Xg33Rfvc2II8UmrZOVdhOWs8ZK0pis9e+\nPo2MKtTzrzipXI2QXv5w7kO+LJWNDva+xRlW8Wlj9Dde9QdQ7Y8+dk7SJgf24DzM\n023elgX5DvTeLODjStk6SMPRL0FmGovUqAAA8ZeHtJzkIr1HROWnQiwnAoGBANez\nhFwKnVoQu0RpBz/i4W0RKIxOwltN2zmlN8KjJPhSy00A7nBUfKLRbcwiSHE98Yi/\nUrXwMwR5QeD2ngngRppddJnpiRfjNjnsaqeqNtpO8AxB3XjpCC5zmHUMFHKvPpDj\n1zU/F44li0YjKcMBebZy9PbfAjrIgJfxhPo/oXiNAoGAfx6gaTjOAp2ZaaZ7Jozc\nkyft/5et1DrR6+P3I4T8bxQncRj1UXfqhxzzOiAVrm3tbCKIIp/JarRCtRGzp9u2\nZPfXGzra6CcSdW3Rkli7/jBCYNynOIl7XjQI8ZnFmq6phwu80ntH07mMeZy4tHff\nQqlLpvQ0i1rDr/Wkexdsnm8CgYBgxha9ILoF/Xm3MJPjEsxmnYsen/tM8XpIu5pv\nxbhBfQvfKWrQlOcyOVnUexEbVVo3KvdVz0VkXW60GpE/BxNGEGXO49rxD6x1gl87\nh/+CJGZIaYiOxaY5CP2+jcPizEL6yG32Yq8TxD5fIkmLRu8vbxX+aIFclDY1dVNe\n3wt3xQKBgGEL0EjwRch+P2V+YHAhbETPrEqJjHRWT95pIdF9XtC8fasSOVH81cLX\nXXsX1FTvOJNwG9Nk8rQjYJXGTb2O/2unaazlYUwxKwVpwuGzz/vhH/roHZBAkIVT\njvpykpn9QMezEdpzj5BEv01QzSYBPzIh5myrpoJIoSW7py7zFG3h\n-----END RSA PRIVATE KEY-----\n',
 			token: '!00000000000000000000000000000001',
 			password: '$2a$08$OPESxR2RE/ZijjGanNKk6ezSqGFitqsbZqTjWUZPLhORMKxHCbc4O', // ilovesakurako
 			profile: {},
diff --git a/tools/migration/node.1522066477.user-account-keypair.js b/tools/migration/node.1522066477.user-account-keypair.js
new file mode 100644
index 0000000000..4a968aae28
--- /dev/null
+++ b/tools/migration/node.1522066477.user-account-keypair.js
@@ -0,0 +1,16 @@
+const { default: User } = require('../../built/api/models/user');
+const { generate } = require('../../built/crypto_key');
+
+const updates = [];
+
+User.find({}).each(function(user) {
+	updates.push(User.update({ _id: user._id }, {
+		$set: {
+			account: {
+				keypair: generate(),
+			}
+		}
+	}));
+}).then(function () {
+	Promise.all(updates)
+}).then(process.exit);