diff --git a/doc/plugin-test-authorize.js b/doc/plugin-test-authorize.js new file mode 100644 index 00000000..339e345a --- /dev/null +++ b/doc/plugin-test-authorize.js @@ -0,0 +1,109 @@ +/* + * Spreed Speak Freely. + * Copyright (C) 2013-2014 struktur AG + * + * This file is part of Spreed Speak Freely. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +define(['angular', 'sjcl'], function(angular, sjcl) { + + return { + + initialize: function(app) { + + var lastNonce = null; + var lastUserid = null; + var disconnectTimeout = null; + + app.run(["$window", "mediaStream", function($window, mediaStream) { + + console.log("Injecting test plugin functions to window ..."); + + $window.testDisconnect = function() { + if (disconnectTimeout) { + $window.clearInterval(disconnectTimeout); + disconnectTimeout = null; + console.info("Stopped disconnector."); + return; + } + disconnectTimeout = $window.setInterval(function() { + console.info("Test disconnect!"); + mediaStream.connector.conn.close(); + }, 10000); + console.info("Started disconnector."); + }; + + $window.testCreateSuserid = function(key, userid) { + + var k = sjcl.codec.utf8String.toBits(key); + var foo = new sjcl.misc.hmac(k, sjcl.hash.sha256) + var expiration = parseInt(((new Date).getTime()/1000)+3600, 10); + var useridCombo = ""+expiration+":"+userid; + var secret = foo.mac(useridCombo); + return [useridCombo, sjcl.codec.base64.fromBits(secret)] + + }; + + $window.testAuthorize = function(useridCombo, secret) { + + console.log("Testing authorize with userid", useridCombo, secret); + var url = mediaStream.url.api("sessions") + "/" + mediaStream.api.id + "/"; + console.log("URL", url); + var data = { + id: mediaStream.api.id, + sid: mediaStream.api.sid, + useridcombo: useridCombo, + secret: secret + } + console.log("Data", data); + $.ajax({ + type: "PATCH", + url: url, + contentType: "application/json", + dataType: "json", + data: JSON.stringify(data), + success: function(data) { + if (data.success) { + lastNonce = data.nonce; + lastUserid = data.userid; + console.log("Retrieved nonce", lastNonce, lastUserid); + } + }, + error: function() { + console.log("error", arguments) + } + }); + + }; + + $window.testAuthenticate = function() { + + if (!lastNonce || !lastUserid) { + console.log("Run testAuthorize first."); + return + } + + mediaStream.api.requestAuthentication(lastUserid, lastNonce); + + }; + + }]); + + } + + } + +}); \ No newline at end of file diff --git a/server.conf.in b/server.conf.in index 0f529dc1..b4b92ecb 100644 --- a/server.conf.in +++ b/server.conf.in @@ -1,35 +1,99 @@ -# Spreed Speak Freely server example configuration +; Spreed Speak Freely server example configuration [http] +; HTTP listener in format ip:port. listen = 127.0.0.1:8080 -#root = /usr/share/spreed-speakfreely-server/www -#readtimeout = 10 -#writetimeout = 10 -#basePath = /some/sub/path/ # Set this when running behind a web server under a sub path. -#maxfd = 32768 # Try to set max open files limit on start (works only when run as root). -#stats = true # Provide stats API at /api/v1/stats (do not enable this in production or unprotected!). -#pprofListen = 127.0.0.1:6060 # See http://golang.org/pkg/net/http/pprof/ for details +; Full path to directory where to find the server web assets. +;root = /usr/share/spreed-speakfreely-server/www +; HTTP socket read timeout in seconds. +;readtimeout = 10 +; HTTP socket write timeout in seconds. +;writetimeout = 10 +; Use basePath if the server does not run on the root path (/) of your server. +;basePath = /some/sub/path/ +; Set maximum number of open files (only works when run as root). +;maxfd = 32768 +; Enable stats API /api/v1/stats for debugging (not for production use!). +;stats = false +; Enable HTTP listener for golang pprof module. See +; http://golang.org/pkg/net/http/pprof/ for details. +;pprofListen = 127.0.0.1:6060 [https] -#listen = 127.0.0.1:8443 -#certificate = server.crt # Full path to certificate. -#key = server.key # Full path to key. -#minVersion = SSLv3 # Minimal supported encryption (SSLv3, TLSv1, TLSv1.1, TLSv1.2). -#readtimeout = 10 -#writetimeout = 10 +; Native HTTPS listener in format ip:port. +;listen = 127.0.0.1:8443 +; Full path to PEM encoded certificate chain. +;certificate = server.crt +; Full path to PEM encoded private key. +;key = server.key +; Mimimal supported encryption standard (SSLv3, TLSv1, TLSv1.1 or TLSv1.2). +;minVersion = SSLv3 +; HTTPS socket read timeout in seconds. +;readtimeout = 10 +; HTTPS socket write timeout in seconds. +;writetimeout = 10 [app] -#title = Spreed Speak Freely -#ver = 1234 # version string to use for static resource -#stunURIs = stun.l.google.com:19302 -#turnURIs = turn:turnserver:port?transport=udp turn:anotherturnserver:port?transport=tcp turns:turnserver:443?transport=tcp -#turnSecret = the-default-turn-shared-secret-do-not-keep -sessionSecret = the-default-secret-do-not-keep-me # Use 32 or 64 bytes random data -#tokenFile = tokens.txt # If set, everyone needs to give one of the tokens to launch the web client. One token per line in the file. -#globalRoom = global # Enables a global room. Users in that room are in all rooms. -#defaultRoomEnabled = true # Set to false to disable default room. -#extra = /usr/share/spreed-speakfreely-server/extra # Extra templates directory. Add .html files to define extra-* template slots here. -#plugin = plugins/example1 # Plugin support. +; HTML page title +;title = Spreed Speak Freely +; Version string to use for static resources. This defaults to the server +; version and should only be changed when you use your own way to invalidate +; long cached static resources. +;ver = 1234 +; STUN server URIs in format host:port. You can provide multiple seperated by +; space. If you do not have one use a public one like stun.l.google.com:19302. +; If you have a TURN server you do not need to set an STUN server as the TURN +; server will normally do STUN too. +;stunURIs = stun.l.google.com:19302 +; TURN server URIs in format host:port?transport=udp|tcp. You can provide +; multiple seperated by space. If you do not have at least one TURN server then +; some users will not be able to use the server as the peer to peer connection +; cannot be established without a TURN server due to firewall reasons. An open +; source TURN server which is fully supported can be found at +; https://code.google.com/p/rfc5766-turn-server/. +;turnURIs = turn:turnserver:port?transport=udp turns:turnserver:443?transport=tcp +; Shared secret authentication for TURN user generation if the TURN server is +; protected (which it should be). +; See http://tools.ietf.org/html/draft-uberti-behave-turn-rest-00 for details. +; A supported TURN server is https://code.google.com/p/rfc5766-turn-server/. +;turnSecret = the-default-turn-shared-secret-do-not-keep +; Session secret to use for session id generator. 32 or 64 bytes of random data +; are recommented. +sessionSecret = the-default-secret-do-not-keep-me +; Full path to a text file containig client tokens which a user needs to enter +; when accessing the web client. Each line in this file represents a valid token. +;tokenFile = tokens.txt +; The name of a global room. If enabled it should be kept secret. Users in that +; room are visible in all other rooms. +;globalRoom = global +; The default room is the room at the root URL of the servers base address and +; all users will join this room if enabled. If it is disabled then a room join +; form will be shown instead. +;defaultRoomEnabled = true +; Full path to an extra templates directory. Templates in this directory ending +; with .html will be parsed on startup and can be used to fill the supported +; extra-* template slots. If the extra folder has a sub folder "static", the +; resources in this static folder will be available as /extra/static/filename +; relative to your servers base URL. +;extra = /usr/share/spreed-speakfreely-server/extra +; URL relative to the servers base path for a plugin javascript file which is +; automatically loaded on web client start for all users. You can put your +; plugin in the extra/static folder (see above) or provide another folder using +; a front end webserver. Check the doc folder for more info about plugins and +; examples. +;plugin = extra/static/myplugin.js [log] -#logfile = /var/log/spreed-speakfreely-server.log +;logfile = /var/log/spreed-speakfreely-server.log + +[users] +; Set to true to enable user functionality. +;enabled = false +; Set authorization mode for users. Currently implemented is the "sharedsecret" +; mode which does validate the userid with a HMAC authentication secret. +; The format goes like this: +; BASE64(HMAC-SHA-256(secret, expirationTimestampInSeconds:userid)) +;mode = sharedsecret +; The shared secred for HMAC validation in "sharedsecret" mode. Best use 32 or +; 64 bytes of random data. +;sharedsecret_secret = some-secret-do-not-keep diff --git a/src/app/spreed-speakfreely-server/main.go b/src/app/spreed-speakfreely-server/main.go index 20ac1a39..ce827ad7 100644 --- a/src/app/spreed-speakfreely-server/main.go +++ b/src/app/spreed-speakfreely-server/main.go @@ -270,6 +270,9 @@ func runner(runtime phoenix.Runtime) error { tokenProvider = TokenFileProvider(tokenFile) } + // Create Users handler. + users := NewUsers(runtime) + // Create configuration data structure. config = NewConfig(title, ver, runtimeVersion, basePath, stunURIs, turnURIs, tokenProvider != nil, globalRoomid, defaultRoomEnabled, plugin) @@ -343,7 +346,9 @@ func runner(runtime phoenix.Runtime) error { api.SetMux(r.PathPrefix("/api/v1/").Subrouter()) api.AddResource(&Rooms{}, "/rooms") api.AddResourceWithWrapper(&Tokens{tokenProvider}, httputils.MakeGzipHandler, "/tokens") - api.AddResource(&Sessions{hub: hub}, "/sessions/{id}/") + if users.Enabled { + api.AddResource(&Sessions{hub: hub, users: users}, "/sessions/{id}/") + } if statsEnabled { api.AddResourceWithWrapper(&Stats{hub: hub}, httputils.MakeGzipHandler, "/stats") log.Println("Stats are enabled!") diff --git a/src/app/spreed-speakfreely-server/session.go b/src/app/spreed-speakfreely-server/session.go index c19bead0..8fdd53d4 100644 --- a/src/app/spreed-speakfreely-server/session.go +++ b/src/app/spreed-speakfreely-server/session.go @@ -205,10 +205,10 @@ type SessionUpdate struct { } type SessionToken struct { - Id string - Sid string - Userid string - Nonce string `json:"Nonce,omitempty"` + Id string // Public session id. + Sid string // Secret session id. + Userid string // Public user id. + Nonce string `json:"Nonce,omitempty"` // User autentication nonce. } func init() { diff --git a/src/app/spreed-speakfreely-server/sessions.go b/src/app/spreed-speakfreely-server/sessions.go index 6baf557e..f6c19182 100644 --- a/src/app/spreed-speakfreely-server/sessions.go +++ b/src/app/spreed-speakfreely-server/sessions.go @@ -24,16 +24,26 @@ package main import ( "encoding/json" "github.com/gorilla/mux" + "log" "net/http" ) type SessionNonce struct { Nonce string `json:"nonce"` + Userid string `json:"userid"` Success bool `json:"success"` } +type SessionNonceRequest struct { + Id string `json:"id"` // Public session id. + Sid string `json:"sid"` // Private session id. + UseridCombo string `json:"useridcombo"` // Public user id as used secret (Expiration:Userid) + Secret string `json:"secret"` // base64(hmac-sha265(SecretKey, UseridCombo)) +} + type Sessions struct { - hub *Hub + hub *Hub + users *Users } // Patch is used to add a userid to a given session (login). @@ -43,8 +53,8 @@ func (sessions *Sessions) Patch(request *http.Request) (int, interface{}, http.H error := false decoder := json.NewDecoder(request.Body) - var st SessionToken - err := decoder.Decode(&st) + var snr SessionNonceRequest + err := decoder.Decode(&snr) if err != nil { error = true } @@ -56,32 +66,42 @@ func (sessions *Sessions) Patch(request *http.Request) (int, interface{}, http.H } // Make sure data matches request. - if id != st.Id { + if id != snr.Id { error = true + log.Println("Session patch failed - request id mismatch.") } // Make sure that we have a Sid. - if st.Sid == "" { + if snr.Sid == "" { error = true + log.Println("Session patch failed - sid empty.") } - // Make sure that we have a user. - if st.Userid == "" { + // Validate with users handler. + userid, err := sessions.users.Handler.Validate(&snr) + if err != nil { error = true + log.Println("Session patch failed - users validation failed.", err) } - // TODO(longsleep): Validate userid. + // Make sure that we have a user. + if userid == "" { + error = true + log.Println("Session patch failed - userid empty.") + } // Make sure Sid matches session. - if !sessions.hub.ValidateSession(st.Id, st.Sid) { + if !sessions.hub.ValidateSession(snr.Id, snr.Sid) { + log.Println("Session patch failed - validation failed.") error = true } var nonce string if !error { - // FIXME(longsleep): Not running this might releal error state with a timing attack. - nonce, err = sessions.hub.sessiontokenHandler(&st) + // FIXME(longsleep): Not running this might reveal error state with a timing attack. + nonce, err = sessions.hub.sessiontokenHandler(&SessionToken{Id: snr.Id, Sid: snr.Sid, Userid: userid}) if err != nil { + log.Println("Session patch failed - handle failed.", err) error = true } } @@ -90,6 +110,7 @@ func (sessions *Sessions) Patch(request *http.Request) (int, interface{}, http.H return 403, NewApiError("session_patch_failed", "Failed to patch session"), http.Header{"Content-Type": {"application/json"}} } - return 200, &SessionNonce{Nonce: nonce, Success: true}, http.Header{"Content-Type": {"application/json"}} + log.Printf("Session patch successfull %s -> %s\n", snr.Id, userid) + return 200, &SessionNonce{Nonce: nonce, Userid: userid, Success: true}, http.Header{"Content-Type": {"application/json"}} } diff --git a/src/app/spreed-speakfreely-server/users.go b/src/app/spreed-speakfreely-server/users.go new file mode 100644 index 00000000..e3589945 --- /dev/null +++ b/src/app/spreed-speakfreely-server/users.go @@ -0,0 +1,112 @@ +/* + * Spreed Speak Freely. + * Copyright (C) 2013-2014 struktur AG + * + * This file is part of Spreed Speak Freely. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +package main + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "errors" + "github.com/strukturag/phoenix" + "log" + "strconv" + "strings" + "time" +) + +type UsersHandler interface { + Validate(snr *SessionNonceRequest) (string, error) +} + +type UsersSharedsecretHandler struct { + secret []byte +} + +func (uh *UsersSharedsecretHandler) Validate(snr *SessionNonceRequest) (string, error) { + + // Parse UseridCombo. + useridCombo := strings.SplitN(snr.UseridCombo, ":", 2) + expirationString, userid := useridCombo[0], useridCombo[1] + + expiration, err := strconv.ParseInt(expirationString, 10, 64) + if err != nil { + return "", err + } + + // Check expiration. + if time.Unix(expiration, 0).Before(time.Now()) { + return "", errors.New("expired secret") + } + + // Check HMAC. + foo := hmac.New(sha256.New, uh.secret) + foo.Write([]byte(snr.UseridCombo)) + fooSecret := base64.StdEncoding.EncodeToString(foo.Sum(nil)) + if snr.Secret != fooSecret { + return "", errors.New("invalid secret") + } + + return userid, nil +} + +type Users struct { + Enabled bool + Handler UsersHandler +} + +func NewUsers(runtime phoenix.Runtime) *Users { + + enabled := false + enabledString, err := runtime.GetString("users", "enabled") + if err == nil { + enabled = enabledString == "true" + } + + var handler UsersHandler + + if enabled { + + mode, _ := runtime.GetString("users", "mode") + switch mode { + case "sharedsecret": + secret, _ := runtime.GetString("users", "sharedsecret_secret") + if secret != "" { + handler = &UsersSharedsecretHandler{secret: []byte(secret)} + } + default: + mode = "" + } + + if handler == nil { + enabled = false + } else { + log.Printf("Enabled users handler '%s'.\n", mode) + } + + } + + return &Users{ + Enabled: enabled, + Handler: handler, + } + +} diff --git a/static/js/base.js b/static/js/base.js index cf778247..88f4df3d 100644 --- a/static/js/base.js +++ b/static/js/base.js @@ -31,5 +31,6 @@ define([ 'audiocontext', 'rAF', 'humanize', - 'sha' + 'sha', + 'sjcl' ], function(){}); diff --git a/static/js/libs/sjcl.js b/static/js/libs/sjcl.js new file mode 100644 index 00000000..c4c5e39d --- /dev/null +++ b/static/js/libs/sjcl.js @@ -0,0 +1,26 @@ +// http://bitwiseshiftleft.github.io/sjcl/ +// ./configure --without-all --with-sha256 --with-sha512 --with-sha1 --with-hmac --with-codecBase64 --with-codecString +// Copyright 2009-2010 Emily Stark, Mike Hamburg, Dan Boneh, Stanford University. +// SJCL is dual-licensed under the GNU GPL version 2.0 or higher, and a 2-clause BSD license. +"use strict";var sjcl={cipher:{},hash:{},keyexchange:{},mode:{},misc:{},codec:{},exception:{corrupt:function(a){this.toString=function(){return"CORRUPT: "+this.message};this.message=a},invalid:function(a){this.toString=function(){return"INVALID: "+this.message};this.message=a},bug:function(a){this.toString=function(){return"BUG: "+this.message};this.message=a},notReady:function(a){this.toString=function(){return"NOT READY: "+this.message};this.message=a}}}; +"undefined"!==typeof module&&module.exports&&(module.exports=sjcl); +sjcl.bitArray={bitSlice:function(a,b,c){a=sjcl.bitArray.l(a.slice(b/32),32-(b&31)).slice(1);return void 0===c?a:sjcl.bitArray.clamp(a,c-b)},extract:function(a,b,c){var e=Math.floor(-b-c&31);return((b+c-1^b)&-32?a[b/32|0]<<32-e^a[b/32+1|0]>>>e:a[b/32|0]>>>e)&(1<>b-1,1));return a},partial:function(a,b,c){return 32===a?b:(c?b|0:b<<32-a)+0x10000000000*a},getPartial:function(a){return Math.round(a/0x10000000000)||32},equal:function(a,b){if(sjcl.bitArray.bitLength(a)!==sjcl.bitArray.bitLength(b))return!1;var c=0,e;for(e=0;e>>b),c=a[d]<<32-b;d=a.length?a[a.length-1]:0;a=sjcl.bitArray.getPartial(d);e.push(sjcl.bitArray.partial(b+a&31,32>>24),d<<=8;return decodeURIComponent(escape(b))},toBits:function(a){a=unescape(encodeURIComponent(a));var b=[],c,e=0;for(c=0;c>>d)>>>26),6>d?(k=a[c]<<6-d,d+=26,c++):(k<<=6,d-=6);for(;e.length&3&&!b;)e+="=";return e},toBits:function(a,b){a=a.replace(/\s|=/g,"");var c=[],e,d=0,h=sjcl.codec.base64.j,k=0,f;b&&(h=h.substr(0,62)+"-_");for(e=0;ef)throw new sjcl.exception.invalid("this isn't base64!");26>>d),k=f<<32-d):(d+=6,k^=f<<32-d)}d&56&&c.push(sjcl.bitArray.partial(d&56,k,1));return c}};sjcl.codec.base64url={fromBits:function(a){return sjcl.codec.base64.fromBits(a,1,1)},toBits:function(a){return sjcl.codec.base64.toBits(a,1)}};sjcl.hash.sha256=function(a){this.d[0]||this.h();a?(this.c=a.c.slice(0),this.b=a.b.slice(0),this.a=a.a):this.reset()};sjcl.hash.sha256.hash=function(a){return(new sjcl.hash.sha256).update(a).finalize()}; +sjcl.hash.sha256.prototype={blockSize:512,reset:function(){this.c=this.f.slice(0);this.b=[];this.a=0;return this},update:function(a){"string"===typeof a&&(a=sjcl.codec.utf8String.toBits(a));var b,c=this.b=sjcl.bitArray.concat(this.b,a);b=this.a;a=this.a=b+sjcl.bitArray.bitLength(a);for(b=512+b&-512;b<=a;b+=512)this.e(c.splice(0,16));return this},finalize:function(){var a,b=this.b,c=this.c,b=sjcl.bitArray.concat(b,[sjcl.bitArray.partial(1,1)]);for(a=b.length+2;a&15;a++)b.push(0);b.push(Math.floor(this.a/ +4294967296));for(b.push(this.a|0);b.length;)this.e(b.splice(0,16));this.reset();return c},f:[],d:[],h:function(){function a(a){return 0x100000000*(a-Math.floor(a))|0}var b=0,c=2,e;a:for(;64>b;c++){for(e=2;e*e<=c;e++)if(0===c%e)continue a;8>b&&(this.f[b]=a(Math.pow(c,0.5)));this.d[b]=a(Math.pow(c,1/3));b++}},e:function(a){var b,c,e=a.slice(0),d=this.c,h=this.d,k=d[0],f=d[1],g=d[2],u=d[3],m=d[4],v=d[5],w=d[6],x=d[7];for(a=0;64>a;a++)16>a?b=e[a]:(b=e[a+1&15],c=e[a+14&15],b=e[a&15]=(b>>>7^b>>>18^b>>>3^ +b<<25^b<<14)+(c>>>17^c>>>19^c>>>10^c<<15^c<<13)+e[a&15]+e[a+9&15]|0),b=b+x+(m>>>6^m>>>11^m>>>25^m<<26^m<<21^m<<7)+(w^m&(v^w))+h[a],x=w,w=v,v=m,m=u+b|0,u=g,g=f,f=k,k=b+(f&g^u&(f^g))+(f>>>2^f>>>13^f>>>22^f<<30^f<<19^f<<10)|0;d[0]=d[0]+k|0;d[1]=d[1]+f|0;d[2]=d[2]+g|0;d[3]=d[3]+u|0;d[4]=d[4]+m|0;d[5]=d[5]+v|0;d[6]=d[6]+w|0;d[7]=d[7]+x|0}};sjcl.hash.sha512=function(a){this.d[0]||this.h();a?(this.c=a.c.slice(0),this.b=a.b.slice(0),this.a=a.a):this.reset()};sjcl.hash.sha512.hash=function(a){return(new sjcl.hash.sha512).update(a).finalize()}; +sjcl.hash.sha512.prototype={blockSize:1024,reset:function(){this.c=this.f.slice(0);this.b=[];this.a=0;return this},update:function(a){"string"===typeof a&&(a=sjcl.codec.utf8String.toBits(a));var b,c=this.b=sjcl.bitArray.concat(this.b,a);b=this.a;a=this.a=b+sjcl.bitArray.bitLength(a);for(b=1024+b&-1024;b<=a;b+=1024)this.e(c.splice(0,32));return this},finalize:function(){var a,b=this.b,c=this.c,b=sjcl.bitArray.concat(b,[sjcl.bitArray.partial(1,1)]);for(a=b.length+4;a&31;a++)b.push(0);b.push(0);b.push(0); +b.push(Math.floor(this.a/0x100000000));for(b.push(this.a|0);b.length;)this.e(b.splice(0,32));this.reset();return c},f:[],n:[12372232,13281083,9762859,1914609,15106769,4090911,4308331,8266105],d:[],o:[2666018,15689165,5061423,9034684,4764984,380953,1658779,7176472,197186,7368638,14987916,16757986,8096111,1480369,13046325,6891156,15813330,5187043,9229749,11312229,2818677,10937475,4324308,1135541,6741931,11809296,16458047,15666916,11046850,698149,229999,945776,13774844,2541862,12856045,9810911,11494366, +7844520,15576806,8533307,15795044,4337665,16291729,5553712,15684120,6662416,7413802,12308920,13816008,4303699,9366425,10176680,13195875,4295371,6546291,11712675,15708924,1519456,15772530,6568428,6495784,8568297,13007125,7492395,2515356,12632583,14740254,7262584,1535930,13146278,16321966,1853211,294276,13051027,13221564,1051980,4080310,6651434,14088940,4675607],h:function(){function a(a){return 0x100000000*(a-Math.floor(a))|0}function b(a){return 0x10000000000*(a-Math.floor(a))&255}var c=0,e=2,d;a:for(;80> +c;e++){for(d=2;d*d<=e;d++)if(0===e%d)continue a;8>c&&(this.f[2*c]=a(Math.pow(e,0.5)),this.f[2*c+1]=b(Math.pow(e,0.5))<<24|this.n[c]);this.d[2*c]=a(Math.pow(e,1/3));this.d[2*c+1]=b(Math.pow(e,1/3))<<24|this.o[c];c++}},e:function(a){var b,c,e=a.slice(0),d=this.c,h=this.d,k=d[0],f=d[1],g=d[2],u=d[3],m=d[4],v=d[5],w=d[6],x=d[7],R=d[8],H=d[9],S=d[10],I=d[11],T=d[12],J=d[13],U=d[14],K=d[15],q=k,n=f,A=g,y=u,B=m,z=v,N=w,C=x,r=R,p=H,L=S,D=I,M=T,E=J,O=U,F=K;for(a=0;80>a;a++){if(16>a)b=e[2*a],c=e[2*a+1];else{c= +e[2*(a-15)];var l=e[2*(a-15)+1];b=(l<<31|c>>>1)^(l<<24|c>>>8)^c>>>7;var s=(c<<31|l>>>1)^(c<<24|l>>>8)^(c<<25|l>>>7);c=e[2*(a-2)];var t=e[2*(a-2)+1],l=(t<<13|c>>>19)^(c<<3|t>>>29)^c>>>6,t=(c<<13|t>>>19)^(t<<3|c>>>29)^(c<<26|t>>>6),P=e[2*(a-7)],Q=e[2*(a-16)],G=e[2*(a-16)+1];c=s+e[2*(a-7)+1];b=b+P+(c>>>0>>0?1:0);c+=t;b+=l+(c>>>0>>0?1:0);c+=G;b+=Q+(c>>>0>>0?1:0)}e[2*a]=b|=0;e[2*a+1]=c|=0;var P=r&L^~r&M,V=p&D^~p&E,t=q&A^q&B^A&B,X=n&y^n&z^y&z,Q=(n<<4|q>>>28)^(q<<30|n>>>2)^(q<<25|n>>>7),G=(q<<4| +n>>>28)^(n<<30|q>>>2)^(n<<25|q>>>7),Y=h[2*a],W=h[2*a+1],l=F+((r<<18|p>>>14)^(r<<14|p>>>18)^(p<<23|r>>>9)),s=O+((p<<18|r>>>14)^(p<<14|r>>>18)^(r<<23|p>>>9))+(l>>>0>>0?1:0),l=l+V,s=s+(P+(l>>>0>>0?1:0)),l=l+W,s=s+(Y+(l>>>0>>0?1:0)),l=l+c,s=s+(b+(l>>>0>>0?1:0));c=G+X;b=Q+t+(c>>>0>>0?1:0);O=M;F=E;M=L;E=D;L=r;D=p;p=C+l|0;r=N+s+(p>>>0>>0?1:0)|0;N=B;C=z;B=A;z=y;A=q;y=n;n=l+c|0;q=s+b+(n>>>0>>0?1:0)|0}f=d[1]=f+n|0;d[0]=k+q+(f>>>0>>0?1:0)|0;u=d[3]=u+y|0;d[2]=g+A+(u>>>0>>0?1:0)|0;v= +d[5]=v+z|0;d[4]=m+B+(v>>>0>>0?1:0)|0;x=d[7]=x+C|0;d[6]=w+N+(x>>>0>>0?1:0)|0;H=d[9]=H+p|0;d[8]=R+r+(H>>>0

>>0?1:0)|0;I=d[11]=I+D|0;d[10]=S+L+(I>>>0>>0?1:0)|0;J=d[13]=J+E|0;d[12]=T+M+(J>>>0>>0?1:0)|0;K=d[15]=K+F|0;d[14]=U+O+(K>>>0>>0?1:0)|0}};sjcl.hash.sha1=function(a){a?(this.c=a.c.slice(0),this.b=a.b.slice(0),this.a=a.a):this.reset()};sjcl.hash.sha1.hash=function(a){return(new sjcl.hash.sha1).update(a).finalize()}; +sjcl.hash.sha1.prototype={blockSize:512,reset:function(){this.c=this.f.slice(0);this.b=[];this.a=0;return this},update:function(a){"string"===typeof a&&(a=sjcl.codec.utf8String.toBits(a));var b,c=this.b=sjcl.bitArray.concat(this.b,a);b=this.a;a=this.a=b+sjcl.bitArray.bitLength(a);for(b=this.blockSize+b&-this.blockSize;b<=a;b+=this.blockSize)this.e(c.splice(0,16));return this},finalize:function(){var a,b=this.b,c=this.c,b=sjcl.bitArray.concat(b,[sjcl.bitArray.partial(1,1)]);for(a=b.length+2;a&15;a++)b.push(0); +b.push(Math.floor(this.a/0x100000000));for(b.push(this.a|0);b.length;)this.e(b.splice(0,16));this.reset();return c},f:[1732584193,4023233417,2562383102,271733878,3285377520],d:[1518500249,1859775393,2400959708,3395469782],e:function(a){var b,c,e,d,h,k,f=a.slice(0),g=this.c;c=g[0];e=g[1];d=g[2];h=g[3];k=g[4];for(a=0;79>=a;a++)16<=a&&(f[a]=(f[a-3]^f[a-8]^f[a-14]^f[a-16])<<1|(f[a-3]^f[a-8]^f[a-14]^f[a-16])>>>31),b=19>=a?e&d|~e&h:39>=a?e^d^h:59>=a?e&d|e&h|d&h:79>=a?e^d^h:void 0,b=(c<<5|c>>>27)+b+k+f[a]+ +this.d[Math.floor(a/20)]|0,k=h,h=d,d=e<<30|e>>>2,e=c,c=b;g[0]=g[0]+c|0;g[1]=g[1]+e|0;g[2]=g[2]+d|0;g[3]=g[3]+h|0;g[4]=g[4]+k|0}};sjcl.misc.hmac=function(a,b){this.k=b=b||sjcl.hash.sha256;var c=[[],[]],e,d=b.prototype.blockSize/32;this.g=[new b,new b];a.length>d&&(a=b.hash(a));for(e=0;e