diff --git a/src/sbbs3/js_cryptcon.c b/src/sbbs3/js_cryptcon.c
index 91ef723b0294b368ec2e3e9504441b420e1c4dd6..4fe6b8a4b015d8264b8997129adbc5eb119e89b9 100644
--- a/src/sbbs3/js_cryptcon.c
+++ b/src/sbbs3/js_cryptcon.c
@@ -5,14 +5,11 @@
 #include "sbbs.h"
 #include <cryptlib.h>
 #include "js_request.h"
+#include "js_cryptcon.h"
 #include "ssl.h"
 #include "base64.h"
 
-struct private_data {
-	CRYPT_CONTEXT	ctx;
-};
-
-static JSClass js_cryptcon_class;
+JSClass js_cryptcon_class;
 static const char* getprivate_failure = "line %d %s %s JS_GetPrivate failed";
 
 // Helpers
@@ -204,7 +201,7 @@ static void js_simple_asn1(unsigned char *data, size_t len, JSContext *cx, JSObj
 
 static void js_create_key_object(JSContext *cx, JSObject *parent)
 {
-	struct private_data* p;
+	struct js_cryptcon_private_data* p;
 	jsrefcount rc;
 	int status;
 	int val;
@@ -212,7 +209,7 @@ static void js_create_key_object(JSContext *cx, JSObject *parent)
 	CRYPT_CERTIFICATE cert;	// Just to hold the public key...
 	unsigned char *certbuf;
 
-	if ((p=(struct private_data *)JS_GetPrivate(cx,parent))==NULL)
+	if ((p=(struct js_cryptcon_private_data *)JS_GetPrivate(cx,parent))==NULL)
 		return;
 
 	rc = JS_SUSPENDREQUEST(cx);
@@ -280,9 +277,9 @@ resume:
 static void
 js_finalize_cryptcon(JSContext *cx, JSObject *obj)
 {
-	struct private_data* p;
+	struct js_cryptcon_private_data* p;
 
-	if ((p=(struct private_data *)JS_GetPrivate(cx,obj))==NULL)
+	if ((p=(struct js_cryptcon_private_data *)JS_GetPrivate(cx,obj))==NULL)
 		return;
 
 	cryptDestroyContext(p->ctx);
@@ -296,12 +293,12 @@ js_finalize_cryptcon(JSContext *cx, JSObject *obj)
 static JSBool
 js_generate_key(JSContext *cx, uintN argc, jsval *arglist)
 {
-	struct private_data* p;
+	struct js_cryptcon_private_data* p;
 	JSObject *obj=JS_THIS_OBJECT(cx, arglist);
 	jsrefcount rc;
 	int status;
 
-	if ((p=(struct private_data *)JS_GetPrivate(cx,obj))==NULL) {
+	if ((p=(struct js_cryptcon_private_data *)JS_GetPrivate(cx,obj))==NULL) {
 		JS_ReportError(cx, getprivate_failure, WHERE);
 		return JS_FALSE;
 	}
@@ -322,7 +319,7 @@ js_generate_key(JSContext *cx, uintN argc, jsval *arglist)
 static JSBool
 js_set_key(JSContext *cx, uintN argc, jsval *arglist)
 {
-	struct private_data* p;
+	struct js_cryptcon_private_data* p;
 	JSObject *obj;
 	jsval *argv;
 	size_t len;
@@ -341,7 +338,7 @@ js_set_key(JSContext *cx, uintN argc, jsval *arglist)
 		return JS_FALSE;
 
 	obj = JS_THIS_OBJECT(cx, arglist);
-	if ((p=(struct private_data *)JS_GetPrivate(cx,obj))==NULL) {
+	if ((p=(struct js_cryptcon_private_data *)JS_GetPrivate(cx,obj))==NULL) {
 		free(key);
 		JS_ReportError(cx, getprivate_failure, WHERE);
 		return JS_FALSE;
@@ -364,7 +361,7 @@ js_set_key(JSContext *cx, uintN argc, jsval *arglist)
 static JSBool
 js_derive_key(JSContext *cx, uintN argc, jsval *arglist)
 {
-	struct private_data* p;
+	struct js_cryptcon_private_data* p;
 	JSObject *obj;
 	jsval *argv;
 	size_t len;
@@ -389,7 +386,7 @@ js_derive_key(JSContext *cx, uintN argc, jsval *arglist)
 	}
 
 	obj = JS_THIS_OBJECT(cx, arglist);
-	if ((p=(struct private_data *)JS_GetPrivate(cx,obj))==NULL) {
+	if ((p=(struct js_cryptcon_private_data *)JS_GetPrivate(cx,obj))==NULL) {
 		free(key);
 		JS_ReportError(cx, getprivate_failure, WHERE);
 		return JS_FALSE;
@@ -412,7 +409,7 @@ js_derive_key(JSContext *cx, uintN argc, jsval *arglist)
 static JSBool
 js_do_encrption(JSContext *cx, uintN argc, jsval *arglist, int encrypt)
 {
-	struct private_data* p;
+	struct js_cryptcon_private_data* p;
 	JSObject *obj;
 	jsval *argv;
 	size_t len;
@@ -427,7 +424,7 @@ js_do_encrption(JSContext *cx, uintN argc, jsval *arglist, int encrypt)
 	argv = JS_ARGV(cx, arglist);
 
 	obj = JS_THIS_OBJECT(cx, arglist);
-	if ((p=(struct private_data *)JS_GetPrivate(cx,obj))==NULL) {
+	if ((p=(struct js_cryptcon_private_data *)JS_GetPrivate(cx,obj))==NULL) {
 		JS_ReportError(cx, getprivate_failure, WHERE);
 		return JS_FALSE;
 	}
@@ -472,8 +469,8 @@ js_decrypt(JSContext *cx, uintN argc, jsval *arglist)
 static JSBool
 js_create_signature(JSContext *cx, uintN argc, jsval *arglist)
 {
-	struct private_data* p;
-	struct private_data* scp;
+	struct js_cryptcon_private_data* p;
+	struct js_cryptcon_private_data* scp;
 	JSObject *sigCtx;
 	JSObject *obj;
 	jsval *argv;
@@ -489,7 +486,7 @@ js_create_signature(JSContext *cx, uintN argc, jsval *arglist)
 	argv = JS_ARGV(cx, arglist);
 
 	obj = JS_THIS_OBJECT(cx, arglist);
-	if ((p=(struct private_data *)JS_GetPrivate(cx,obj))==NULL) {
+	if ((p=(struct js_cryptcon_private_data *)JS_GetPrivate(cx,obj))==NULL) {
 		JS_ReportError(cx, getprivate_failure, WHERE);
 		return JS_FALSE;
 	}
@@ -497,7 +494,7 @@ js_create_signature(JSContext *cx, uintN argc, jsval *arglist)
 	if (!JS_InstanceOf(cx, sigCtx, &js_cryptcon_class, NULL))
 		return JS_FALSE;
 	HANDLE_PENDING(cx, NULL);
-	if ((scp=(struct private_data *)JS_GetPrivate(cx,sigCtx))==NULL) {
+	if ((scp=(struct js_cryptcon_private_data *)JS_GetPrivate(cx,sigCtx))==NULL) {
 		JS_ReportError(cx, getprivate_failure, WHERE);
 		return JS_FALSE;
 	}
@@ -634,9 +631,9 @@ js_cryptcon_set(JSContext *cx, JSObject *obj, jsid id, JSBool strict, jsval *vp)
 {
 	jsval idval;
     jsint tiny;
-	struct private_data* p;
+	struct js_cryptcon_private_data* p;
 
-	if ((p=(struct private_data *)JS_GetPrivate(cx,obj))==NULL) {
+	if ((p=(struct js_cryptcon_private_data *)JS_GetPrivate(cx,obj))==NULL) {
 		JS_ReportError(cx, getprivate_failure, WHERE);
 		return JS_FALSE;
 	}
@@ -731,9 +728,9 @@ js_cryptcon_get(JSContext *cx, JSObject *obj, jsid id, jsval *vp)
 {
 	jsval idval;
     jsint tiny;
-	struct private_data* p;
+	struct js_cryptcon_private_data* p;
 
-	if ((p=(struct private_data *)JS_GetPrivate(cx,obj))==NULL) {
+	if ((p=(struct js_cryptcon_private_data *)JS_GetPrivate(cx,obj))==NULL) {
 		return JS_TRUE;
 		JS_ReportError(cx, getprivate_failure, WHERE);
 		return JS_FALSE;
@@ -846,7 +843,7 @@ static JSBool js_cryptcon_enumerate(JSContext *cx, JSObject *obj)
 	return(js_cryptcon_resolve(cx, obj, JSID_VOID));
 }
 
-static JSClass js_cryptcon_class = {
+JSClass js_cryptcon_class = {
      "CryptContext"			/* name			*/
     ,JSCLASS_HAS_PRIVATE	/* flags		*/
 	,JS_PropertyStub		/* addProperty	*/
@@ -859,6 +856,29 @@ static JSClass js_cryptcon_class = {
 	,js_finalize_cryptcon		/* finalize		*/
 };
 
+JSObject* DLLCALL js_CreateCryptconObject(JSContext* cx, CRYPT_CONTEXT ctx)
+{
+	JSObject *obj;
+	struct js_cryptcon_private_data *p;
+
+	obj=JS_NewObject(cx, &js_cryptcon_class, NULL, NULL);
+
+	if((p=(struct js_cryptcon_private_data *)malloc(sizeof(struct js_cryptcon_private_data)))==NULL) {
+		JS_ReportError(cx,"malloc failed");
+		return NULL;
+	}
+	memset(p,0,sizeof(struct js_cryptcon_private_data));
+	p->ctx = ctx;
+
+	if(!JS_SetPrivate(cx, obj, p)) {
+		JS_ReportError(cx,"JS_SetPrivate failed");
+		return NULL;
+	}
+	js_create_key_object(cx, obj);
+
+	return obj;
+}
+
 // Constructor
 
 static JSBool
@@ -866,7 +886,7 @@ js_cryptcon_constructor(JSContext *cx, uintN argc, jsval *arglist)
 {
 	JSObject *obj;
 	jsval *argv=JS_ARGV(cx, arglist);
-	struct private_data *p;
+	struct js_cryptcon_private_data *p;
 	jsrefcount rc;
 	int status;
 	int algo;
@@ -881,11 +901,11 @@ js_cryptcon_constructor(JSContext *cx, uintN argc, jsval *arglist)
 	if (!JS_ValueToInt32(cx,argv[0],&algo))
 		return JS_FALSE;
 
-	if((p=(struct private_data *)malloc(sizeof(struct private_data)))==NULL) {
+	if((p=(struct js_cryptcon_private_data *)malloc(sizeof(struct js_cryptcon_private_data)))==NULL) {
 		JS_ReportError(cx,"malloc failed");
 		return(JS_FALSE);
 	}
-	memset(p,0,sizeof(struct private_data));
+	memset(p,0,sizeof(struct js_cryptcon_private_data));
 
 	if(!JS_SetPrivate(cx, obj, p)) {
 		JS_ReportError(cx,"JS_SetPrivate failed");
diff --git a/src/sbbs3/js_cryptcon.h b/src/sbbs3/js_cryptcon.h
new file mode 100644
index 0000000000000000000000000000000000000000..c9331176b70fdb409fddc77d7a528041b10f38f8
--- /dev/null
+++ b/src/sbbs3/js_cryptcon.h
@@ -0,0 +1,12 @@
+#ifndef _JS_CRYPTCON_H_
+#define _JS_CRYPTCON_H_
+
+struct js_cryptcon_private_data {
+	CRYPT_CONTEXT	ctx;
+};
+
+extern JSClass js_cryptcon_class;
+
+JSObject* DLLCALL js_CreateCryptconObject(JSContext* cx, CRYPT_CONTEXT ctx);
+
+#endif
diff --git a/src/sbbs3/js_cryptkeyset.c b/src/sbbs3/js_cryptkeyset.c
new file mode 100644
index 0000000000000000000000000000000000000000..14a4905b298199de772fa3d49802b20c90376589
--- /dev/null
+++ b/src/sbbs3/js_cryptkeyset.c
@@ -0,0 +1,446 @@
+/* $Id$ */
+
+// Cyrptlib Keyset...
+
+#include "sbbs.h"
+#include <cryptlib.h>
+#include "js_request.h"
+#include "js_cryptcon.h"
+#include "ssl.h"
+
+struct private_data {
+	CRYPT_KEYSET	ks;
+	char			*name;
+};
+
+static JSClass js_cryptkeyset_class;
+static const char* getprivate_failure = "line %d %s %s JS_GetPrivate failed";
+
+// Helpers
+
+// Destructor
+
+static void
+js_finalize_cryptkeyset(JSContext *cx, JSObject *obj)
+{
+	jsrefcount rc;
+	struct private_data* p;
+
+	if ((p=(struct private_data *)JS_GetPrivate(cx,obj))==NULL)
+		return;
+
+	rc = JS_SUSPENDREQUEST(cx);
+	if (p->ks != CRYPT_UNUSED)
+		cryptKeysetClose(p->ks);
+	FREE_AND_NULL(p->name);
+	free(p);
+	JS_RESUMEREQUEST(cx, rc);
+
+	JS_SetPrivate(cx, obj, NULL);
+}
+
+// Methods
+
+static JSBool
+js_add_private_key(JSContext *cx, uintN argc, jsval *arglist)
+{
+	struct private_data* p;
+	struct js_cryptcon_private_data* ctx;
+	jsval *argv=JS_ARGV(cx, arglist);
+	char* pw = NULL;
+	int status;
+	jsrefcount rc;
+	JSObject *key;
+	JSString *jspw;
+	JSObject *obj=JS_THIS_OBJECT(cx, arglist);
+
+	if (!js_argc(cx, argc, 2))
+		return JS_FALSE;
+	if (argc > 2) {
+		JS_ReportError(cx, "Too many arguments");
+		return JS_FALSE;
+	}
+	key = JSVAL_TO_OBJECT(argv[0]);
+	if (!JS_InstanceOf(cx, key, &js_cryptcon_class, NULL)) {
+		JS_ReportError(cx, "Invalid CryptContext");
+		return JS_FALSE;
+	}
+
+	argv = JS_ARGV(cx, arglist);
+	if ((jspw = JS_ValueToString(cx, argv[1])) == NULL) {
+		JS_ReportError(cx, "Invalid password");
+		return JS_FALSE;
+	}
+
+	if ((p=(struct private_data *)JS_GetPrivate(cx,obj))==NULL) {
+		JS_ReportError(cx, getprivate_failure, WHERE);
+		return JS_FALSE;
+	}
+
+	if ((ctx=(struct js_cryptcon_private_data *)JS_GetPrivate(cx,key))==NULL) {
+		JS_ReportError(cx, getprivate_failure, WHERE);
+		return JS_FALSE;
+	}
+
+	JSSTRING_TO_MSTRING(cx, jspw, pw, NULL);
+	HANDLE_PENDING(cx, pw);
+	rc = JS_SUSPENDREQUEST(cx);
+	status = cryptAddPrivateKey(p->ks, ctx->ctx, pw);
+	free(pw);
+	JS_RESUMEREQUEST(cx, rc);
+
+	if (cryptStatusError(status)) {
+		JS_ReportError(cx, "Error %d calling cryptAddPrivateKey()\n", status);
+		return JS_FALSE;
+	}
+	return JS_TRUE;
+}
+
+static JSBool
+js_close(JSContext *cx, uintN argc, jsval *arglist)
+{
+	struct private_data* p;
+	JSObject *obj=JS_THIS_OBJECT(cx, arglist);
+	jsrefcount rc;
+	int status;
+
+	if (argc) {
+		JS_ReportError(cx, "close() takes no arguments");
+		return JS_FALSE;
+	}
+	if ((p=(struct private_data *)JS_GetPrivate(cx,obj))==NULL) {
+		JS_ReportError(cx, getprivate_failure, WHERE);
+		return JS_FALSE;
+	}
+	if (p->ks == CRYPT_UNUSED) {
+		JS_ReportError(cx, "already closed");
+		return JS_FALSE;
+	}
+	rc = JS_SUSPENDREQUEST(cx);
+	status = cryptKeysetClose(p->ks);
+	JS_RESUMEREQUEST(cx, rc);
+	if (cryptStatusError(status)) {
+		JS_ReportError(cx, "Error %d calling cryptKeysetClose()\n", status);
+		return JS_FALSE;
+	}
+
+	return JS_TRUE;
+}
+
+static JSBool
+js_delete_key(JSContext *cx, uintN argc, jsval *arglist)
+{
+	struct private_data* p;
+	jsval *argv=JS_ARGV(cx, arglist);
+	int status;
+	jsrefcount rc;
+	char* label = NULL;
+	JSString *jslabel;
+	JSObject *obj=JS_THIS_OBJECT(cx, arglist);
+
+	if (!js_argc(cx, argc, 1))
+		return JS_FALSE;
+	if (argc > 1) {
+		JS_ReportError(cx, "Too many arguments");
+		return JS_FALSE;
+	}
+	if ((jslabel = JS_ValueToString(cx, argv[0])) == NULL) {
+		JS_ReportError(cx, "Invalid label");
+		return JS_FALSE;
+	}
+
+	if ((p=(struct private_data *)JS_GetPrivate(cx,obj))==NULL) {
+		JS_ReportError(cx, getprivate_failure, WHERE);
+		return JS_FALSE;
+	}
+
+	JSSTRING_TO_MSTRING(cx, jslabel, label, NULL);
+	HANDLE_PENDING(cx, label);
+	rc = JS_SUSPENDREQUEST(cx);
+	status = cryptDeleteKey(p->ks, CRYPT_KEYID_NAME, label);
+fprintf(stderr, "cryptDeleteKey(%s) = %d\n", label, status);
+	free(label);
+	JS_RESUMEREQUEST(cx, rc);
+	if (cryptStatusError(status)) {
+		JS_ReportError(cx, "Error %d calling cryptDeleteKey()\n", status);
+		return JS_FALSE;
+	}
+	return JS_TRUE;
+}
+
+static JSBool
+js_get_private_key(JSContext *cx, uintN argc, jsval *arglist)
+{
+	struct private_data* p;
+	jsval *argv=JS_ARGV(cx, arglist);
+	int status;
+	jsrefcount rc;
+	JSObject *key;
+	char* pw = NULL;
+	char* label = NULL;
+	JSString *jspw;
+	JSString *jslabel;
+	CRYPT_CONTEXT ctx;
+	JSObject *obj=JS_THIS_OBJECT(cx, arglist);
+
+	if (!js_argc(cx, argc, 2))
+		return JS_FALSE;
+	if (argc > 2) {
+		JS_ReportError(cx, "Too many arguments");
+		return JS_FALSE;
+	}
+	if ((jslabel = JS_ValueToString(cx, argv[0])) == NULL) {
+		JS_ReportError(cx, "Invalid label");
+		return JS_FALSE;
+	}
+	if ((jspw = JS_ValueToString(cx, argv[1])) == NULL) {
+		JS_ReportError(cx, "Invalid password");
+		return JS_FALSE;
+	}
+
+	if ((p=(struct private_data *)JS_GetPrivate(cx,obj))==NULL) {
+		JS_ReportError(cx, getprivate_failure, WHERE);
+		return JS_FALSE;
+	}
+
+	JSSTRING_TO_MSTRING(cx, jslabel, label, NULL);
+	HANDLE_PENDING(cx, label);
+	JSSTRING_TO_MSTRING(cx, jspw, pw, NULL);
+	if (JS_IsExceptionPending(cx)) {
+		FREE_AND_NULL(label);
+		FREE_AND_NULL(pw);
+		return JS_FALSE;
+	}
+	rc = JS_SUSPENDREQUEST(cx);
+	status = cryptGetPrivateKey(p->ks, &ctx, CRYPT_KEYID_NAME, label, pw);
+	free(label);
+	free(pw);
+	JS_RESUMEREQUEST(cx, rc);
+	if (cryptStatusError(status)) {
+		JS_ReportError(cx, "Error %d calling cryptGetPrivateKey()\n", status);
+		return JS_FALSE;
+	}
+	key = js_CreateCryptconObject(cx, ctx);
+	if (key == NULL)
+		return JS_FALSE;
+	JS_SET_RVAL(cx, arglist, OBJECT_TO_JSVAL(key));
+
+	return JS_TRUE;
+}
+
+// Properties
+
+static JSBool
+js_cryptkeyset_get(JSContext *cx, JSObject *obj, jsid id, jsval *vp)
+{
+	jsval idval;
+    jsint tiny;
+	struct private_data* p;
+
+	if ((p=(struct private_data *)JS_GetPrivate(cx,obj))==NULL) {
+		return JS_TRUE;
+		JS_ReportError(cx, getprivate_failure, WHERE);
+		return JS_FALSE;
+	}
+
+    JS_IdToValue(cx, id, &idval);
+    tiny = JSVAL_TO_INT(idval);
+
+	switch (tiny) {
+	}
+	return JS_TRUE;
+}
+
+static JSBool
+js_cryptkeyset_set(JSContext *cx, JSObject *obj, jsid id, JSBool strict, jsval *vp)
+{
+	jsval idval;
+    jsint tiny;
+	struct private_data* p;
+
+	if ((p=(struct private_data *)JS_GetPrivate(cx,obj))==NULL) {
+		JS_ReportError(cx, getprivate_failure, WHERE);
+		return JS_FALSE;
+	}
+
+    JS_IdToValue(cx, id, &idval);
+    tiny = JSVAL_TO_INT(idval);
+
+	switch (tiny) {
+	}
+	return JS_TRUE;
+}
+
+
+#ifdef BUILD_JSDOCS
+static char* cryptkeyset_prop_desc[] = {
+	 "Keyopt constant (CryptKeyset.KEYOPT.XXX):<ul class=\"showList\">\n"
+	 "<li>CryptKeyset.KEYOPT.NONE</li>\n"
+	 "<li>CryptKeyset.KEYOPT.READONLY</li>\n"
+	 "<li>CryptKeyset.KEYOPT.CREATE</li>\n"
+	,NULL
+};
+#endif
+
+static jsSyncPropertySpec js_cryptkeyset_properties[] = {
+/*	name				,tinyid						,flags,				ver	*/
+	{0}
+};
+
+static jsSyncMethodSpec js_cryptkeyset_functions[] = {
+	{"add_private_key",	js_add_private_key,	0,	JSTYPE_VOID,	"CryptContext, password"
+	,JSDOCSTR("Add a private key to the keyset, encrypting it with <password>.")
+	,316
+	},
+	{"close",			js_close,			0,	JSTYPE_VOID,	""
+	,JSDOCSTR("Close the keyset.")
+	,316
+	},
+	{"delete_key",		js_delete_key,		0,	JSTYPE_VOID,	"label"
+	,JSDOCSTR("Delete the key with <label> from the keyset.")
+	,316
+	},
+	{"get_private_key",	js_get_private_key,	0,	JSTYPE_OBJECT,	"label, password"
+	,JSDOCSTR("Returns a CryptContext from the private key with <label> encrypted with <password>.")
+	,316
+	},
+	{0}
+};
+
+static JSBool js_cryptkeyset_resolve(JSContext *cx, JSObject *obj, jsid id)
+{
+	char*			name=NULL;
+	JSBool			ret;
+
+	if(id != JSID_VOID && id != JSID_EMPTY) {
+		jsval idval;
+		
+		JS_IdToValue(cx, id, &idval);
+		if(JSVAL_IS_STRING(idval)) {
+			JSSTRING_TO_MSTRING(cx, JSVAL_TO_STRING(idval), name, NULL);
+			HANDLE_PENDING(cx, name);
+		}
+	}
+
+	ret=js_SyncResolve(cx, obj, name, js_cryptkeyset_properties, js_cryptkeyset_functions, NULL, 0);
+	if(name)
+		free(name);
+	return ret;
+}
+
+static JSBool js_cryptkeyset_enumerate(JSContext *cx, JSObject *obj)
+{
+	return(js_cryptkeyset_resolve(cx, obj, JSID_VOID));
+}
+
+static JSClass js_cryptkeyset_class = {
+     "CryptKeyset"				/* name			*/
+    ,JSCLASS_HAS_PRIVATE		/* flags		*/
+	,JS_PropertyStub			/* addProperty	*/
+	,JS_PropertyStub			/* delProperty	*/
+	,js_cryptkeyset_get			/* getProperty	*/
+	,js_cryptkeyset_set			/* setProperty	*/
+	,js_cryptkeyset_enumerate	/* enumerate	*/
+	,js_cryptkeyset_resolve		/* resolve		*/
+	,JS_ConvertStub				/* convert		*/
+	,js_finalize_cryptkeyset	/* finalize		*/
+};
+
+// Constructor
+
+static JSBool
+js_cryptkeyset_constructor(JSContext *cx, uintN argc, jsval *arglist)
+{
+	JSObject *obj;
+	jsval *argv=JS_ARGV(cx, arglist);
+	struct private_data *p;
+	jsrefcount rc;
+	int status;
+	int opts = CRYPT_KEYOPT_NONE;
+	JSString *fn;
+	size_t fnslen;
+
+	do_cryptInit();
+	obj=JS_NewObject(cx, &js_cryptkeyset_class, NULL, NULL);
+	JS_SET_RVAL(cx, arglist, OBJECT_TO_JSVAL(obj));
+	if(argc < 1 || argc > 2) {
+		JS_ReportError(cx, "Incorrect number of arguments to CryptKeyset constructor.  Got %d, expected 1 or 2.", argc);
+		return JS_FALSE;
+	}
+	if ((fn = JS_ValueToString(cx, argv[0])) == NULL)
+		return JS_FALSE;
+	if (argc == 2)
+		if (!JS_ValueToInt32(cx,argv[1],&opts))
+			return JS_FALSE;
+
+	if((p=(struct private_data *)malloc(sizeof(struct private_data)))==NULL) {
+		JS_ReportError(cx,"malloc failed");
+		return(JS_FALSE);
+	}
+	memset(p,0,sizeof(struct private_data));
+
+	if(!JS_SetPrivate(cx, obj, p)) {
+		JS_ReportError(cx,"JS_SetPrivate failed");
+		return(JS_FALSE);
+	}
+
+	JSSTRING_TO_MSTRING(cx, fn, p->name, &fnslen);
+	if (p->name == NULL) {
+		free(p);
+		return JS_FALSE;
+	}
+	rc = JS_SUSPENDREQUEST(cx);
+fprintf(stderr, "Name: %s, opts: %02x\n", p->name, opts);
+	status = cryptKeysetOpen(&p->ks, CRYPT_UNUSED, CRYPT_KEYSET_FILE, p->name, opts);
+	JS_RESUMEREQUEST(cx, rc);
+	if (cryptStatusError(status)) {
+		JS_ReportError(cx, "CryptLib error %d", status);
+		FREE_AND_NULL(p->name);
+		free(p);
+		return JS_FALSE;
+	}
+
+#ifdef BUILD_JSDOCS
+	js_DescribeSyncObject(cx,obj,"Class used for storing CryptContext keys",31601);
+	js_DescribeSyncConstructor(cx,obj,"To create a new CryptKeyset object: "
+		"var c = new CryptKeyset('<i>filename</i>' [ <i>opts = CryptKeyset.KEYOPT.NONE</i> ])</tt><br>"
+		"where <i>filename</i> is the name of the file to create, and <i>opts</i>"
+		"is a value from from CryptKeyset.KEYOPT"
+		);
+	js_CreateArrayOfStrings(cx, obj, "_property_desc_list", cryptkeyset_prop_desc, JSPROP_READONLY);
+#endif
+
+	return(JS_TRUE);
+}
+
+JSObject* DLLCALL js_CreateCryptKeysetClass(JSContext* cx, JSObject* parent)
+{
+	JSObject*	cksobj;
+	JSObject*	constructor;
+	JSObject*	opts;
+	jsval		val;
+
+	cksobj = JS_InitClass(cx, parent, NULL
+		,&js_cryptkeyset_class
+		,js_cryptkeyset_constructor
+		,1	/* number of constructor args */
+		,NULL /* props, specified in constructor */
+		,NULL /* funcs, specified in constructor */
+		,NULL, NULL);
+
+	if(JS_GetProperty(cx, parent, js_cryptkeyset_class.name, &val) && !JSVAL_NULL_OR_VOID(val)) {
+		JS_ValueToObject(cx,val,&constructor);
+		opts = JS_DefineObject(cx, constructor, "KEYOPT", NULL, NULL, JSPROP_PERMANENT|JSPROP_ENUMERATE|JSPROP_READONLY);
+		if(opts != NULL) {
+			JS_DefineProperty(cx, opts, "NONE", INT_TO_JSVAL(CRYPT_KEYOPT_NONE), NULL, NULL
+				, JSPROP_PERMANENT|JSPROP_ENUMERATE|JSPROP_READONLY);
+			JS_DefineProperty(cx, opts, "READONLY", INT_TO_JSVAL(CRYPT_KEYOPT_READONLY), NULL, NULL
+				, JSPROP_PERMANENT|JSPROP_ENUMERATE|JSPROP_READONLY);
+			JS_DefineProperty(cx, opts, "CREATE", INT_TO_JSVAL(CRYPT_KEYOPT_CREATE), NULL, NULL
+				, JSPROP_PERMANENT|JSPROP_ENUMERATE|JSPROP_READONLY);
+			JS_DeepFreezeObject(cx, opts);
+		}
+	}
+
+	return(cksobj);
+}
diff --git a/src/sbbs3/jsdoor.c b/src/sbbs3/jsdoor.c
index 7ee36ec7b6a77102fd12ea65cfe30c29e5dba7be..b5d656355d77e754b6dbf42b3e51b258e82e8e80 100644
--- a/src/sbbs3/jsdoor.c
+++ b/src/sbbs3/jsdoor.c
@@ -254,6 +254,10 @@ BOOL DLLCALL js_CreateCommonObjects(JSContext* js_cx
 		if(js_CreateCryptContextClass(js_cx, *glob)==NULL)
 			break;
 
+		/* CryptKeyset Class */
+		if(js_CreateCryptKeysetClass(js_cx, *glob)==NULL)
+			break;
+
 		success=TRUE;
 	} while(0);
 
diff --git a/src/sbbs3/main.cpp b/src/sbbs3/main.cpp
index 267b457f5355ddca89cc6db7748573154d092ae8..8712a440ac21d482931a8805cf35a0845194a29f 100644
--- a/src/sbbs3/main.cpp
+++ b/src/sbbs3/main.cpp
@@ -1366,6 +1366,10 @@ extern "C" BOOL DLLCALL js_CreateCommonObjects(JSContext* js_cx
 		if(js_CreateCryptContextClass(js_cx, *glob)==NULL)
 			break;
 
+		/* CryptKeyset Class */
+		if(js_CreateCryptKeysetClass(js_cx, *glob)==NULL)
+			break;
+
 		/* Area Objects */
 		if(!js_CreateUserObjects(js_cx, *glob, cfg, /* user: */NULL, client, /* html_index_fname: */NULL, /* subscan: */NULL)) 
 			break;
diff --git a/src/sbbs3/objects.mk b/src/sbbs3/objects.mk
index 5b20a1c0afcae090ebd35c63996aaa27e5395f22..2aaffaf5f7ceae8162799e57d48ef85714f8ca22 100644
--- a/src/sbbs3/objects.mk
+++ b/src/sbbs3/objects.mk
@@ -46,6 +46,7 @@ OBJS	=	$(MTOBJODIR)$(DIRSEP)ansiterm$(OFILE) \
 			$(MTOBJODIR)$(DIRSEP)js_com$(OFILE)\
 			$(MTOBJODIR)$(DIRSEP)js_console$(OFILE)\
 			$(MTOBJODIR)$(DIRSEP)js_cryptcon$(OFILE)\
+			$(MTOBJODIR)$(DIRSEP)js_cryptkeyset$(OFILE)\
 			$(MTOBJODIR)$(DIRSEP)js_file$(OFILE)\
 			$(MTOBJODIR)$(DIRSEP)js_file_area$(OFILE)\
 			$(MTOBJODIR)$(DIRSEP)js_global$(OFILE)\
diff --git a/src/sbbs3/sbbs.h b/src/sbbs3/sbbs.h
index 4c57d3c6ea3b4fd5b1824e69027eb4ceec47a103..a2d11d4a79542c3d80a6167a49836c908b7b283b 100644
--- a/src/sbbs3/sbbs.h
+++ b/src/sbbs3/sbbs.h
@@ -1334,6 +1334,9 @@ extern "C" {
 	/* js_cryptcon.c */
 	DLLEXPORT JSObject* DLLCALL js_CreateCryptContextClass(JSContext* cx, JSObject* parent);
 
+	/* js_cryptkeyset.c */
+	DLLEXPORT JSObject* DLLCALL js_CreateCryptKeysetClass(JSContext* cx, JSObject* parent);
+
 #endif
 
 /* str_util.c */
diff --git a/src/sbbs3/services.c b/src/sbbs3/services.c
index 9cfec055f916d39b72fec07bfb9116ba7baa0b94..da2154f5d70d30381b20ec2beb1e84a84b2dd1ff 100644
--- a/src/sbbs3/services.c
+++ b/src/sbbs3/services.c
@@ -803,6 +803,10 @@ js_initcx(JSRuntime* js_runtime, SOCKET sock, service_client_t* service_client,
 		if(js_CreateCryptContextClass(js_cx, *glob)==NULL)
 			break;
 
+		/* CryptKeyset Class */
+		if(js_CreateCryptKeysetClass(js_cx, *glob)==NULL)
+			break;
+
 		/* user-specific objects */
 		if(!js_CreateUserObjects(js_cx, *glob, &scfg, /*user: */NULL, service_client->client, NULL, service_client->subscan)) 
 			break;