Skip to content

WIP: WebcryptoAPI support #558

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 41 additions & 11 deletions build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,23 @@ func ImportDir(dir string, mode build.ImportMode, installSuffix string, buildTag
return &PackageData{Package: pkg, JSFiles: jsFiles}, nil
}

// Test if we find the '//gopherjs:keep_overridden' comment
func findKeepOverridenComment(doc *ast.CommentGroup) bool {
if doc == nil {
return false
}
for _, comment := range doc.List {
text := comment.Text
if i := strings.Index(text, " "); i >= 0 {
text = text[:i]
}
if text == "//gopherjs:keep_overridden" {
return true
}
}
return false
}

// parseAndAugment parses and returns all .go files of given pkg.
// Standard Go library packages are augmented with files in compiler/natives folder.
// If isTest is true and pkg.ImportPath has no _test suffix, package is built for running internal tests.
Expand All @@ -195,12 +212,19 @@ func ImportDir(dir string, mode build.ImportMode, installSuffix string, buildTag
// The native packages are augmented by the contents of natives.FS in the following way.
// The file names do not matter except the usual `_test` suffix. The files for
// native overrides get added to the package (even if they have the same name
// as an existing file from the standard library). For all identifiers that exist
// in the original AND the overrides, the original identifier in the AST gets
// replaced by `_`. New identifiers that don't exist in original package get added.
// as an existing file from the standard library). For function identifiers that exist
// in the original AND the overrides AND that include the following directive in their comment:
// //gopherjs:keep_overridden, the original identifier in the AST gets prefixed by
// `_gopherjs_overridden_`. For other identifiers that exist in the original AND the overrides,
// the original identifier gets replaced by `_`. New identifiers that don't exist in original
// package get added.
func parseAndAugment(pkg *build.Package, isTest bool, fileSet *token.FileSet) ([]*ast.File, error) {
var files []*ast.File
replacedDeclNames := make(map[string]bool)

type overrideInfo struct {
keepOverriden bool
}
replacedDeclNames := make(map[string]overrideInfo)
funcName := func(d *ast.FuncDecl) string {
if d.Recv == nil || len(d.Recv.List) == 0 {
return d.Name.Name
Expand Down Expand Up @@ -279,17 +303,17 @@ func parseAndAugment(pkg *build.Package, isTest bool, fileSet *token.FileSet) ([
for _, decl := range file.Decls {
switch d := decl.(type) {
case *ast.FuncDecl:
replacedDeclNames[funcName(d)] = true
replacedDeclNames[funcName(d)] = overrideInfo{keepOverriden: findKeepOverridenComment(d.Doc)}
case *ast.GenDecl:
switch d.Tok {
case token.TYPE:
for _, spec := range d.Specs {
replacedDeclNames[spec.(*ast.TypeSpec).Name.Name] = true
replacedDeclNames[spec.(*ast.TypeSpec).Name.Name] = overrideInfo{}
}
case token.VAR, token.CONST:
for _, spec := range d.Specs {
for _, name := range spec.(*ast.ValueSpec).Names {
replacedDeclNames[name.Name] = true
replacedDeclNames[name.Name] = overrideInfo{}
}
}
}
Expand Down Expand Up @@ -341,23 +365,29 @@ func parseAndAugment(pkg *build.Package, isTest bool, fileSet *token.FileSet) ([
for _, decl := range file.Decls {
switch d := decl.(type) {
case *ast.FuncDecl:
if replacedDeclNames[funcName(d)] {
d.Name = ast.NewIdent("_")
if info, ok := replacedDeclNames[funcName(d)]; ok {
if info.keepOverriden {
// Allow overridden function calls
// The standard library implementation of foo() becomes _gopherjs_overridden_foo()
d.Name.Name = "_gopherjs_overridden_" + d.Name.Name
} else {
d.Name = ast.NewIdent("_")
}
}
case *ast.GenDecl:
switch d.Tok {
case token.TYPE:
for _, spec := range d.Specs {
s := spec.(*ast.TypeSpec)
if replacedDeclNames[s.Name.Name] {
if _, ok := replacedDeclNames[s.Name.Name]; ok {
s.Name = ast.NewIdent("_")
}
}
case token.VAR, token.CONST:
for _, spec := range d.Specs {
s := spec.(*ast.ValueSpec)
for i, name := range s.Names {
if replacedDeclNames[name.Name] {
if _, ok := replacedDeclNames[name.Name]; ok {
s.Names[i] = ast.NewIdent("_")
}
}
Expand Down
6 changes: 3 additions & 3 deletions build/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ func TestNativesDontImportExtraPackages(t *testing.T) {
if err != nil {
t.Fatalf("strconv.Unquote(%v): %v", imp.Path.Value, err)
}
if importPath == "github.com/gopherjs/gopherjs/js" {
if strings.HasPrefix(importPath, "github.com/gopherjs/gopherjs/js") {
continue
}
if _, ok := realImports[importPath]; !ok {
Expand Down Expand Up @@ -133,7 +133,7 @@ func TestNativesDontImportExtraPackages(t *testing.T) {
if err != nil {
t.Fatalf("strconv.Unquote(%v): %v", imp.Path.Value, err)
}
if importPath == "github.com/gopherjs/gopherjs/js" {
if strings.HasPrefix(importPath, "github.com/gopherjs/gopherjs/js") {
continue
}
if _, ok := realTestImports[importPath]; !ok {
Expand Down Expand Up @@ -175,7 +175,7 @@ func TestNativesDontImportExtraPackages(t *testing.T) {
if err != nil {
t.Fatalf("strconv.Unquote(%v): %v", imp.Path.Value, err)
}
if importPath == "github.com/gopherjs/gopherjs/js" {
if strings.HasPrefix(importPath, "github.com/gopherjs/gopherjs/js") {
continue
}
if _, ok := realXTestImports[importPath]; !ok {
Expand Down
217 changes: 116 additions & 101 deletions compiler/natives/fs_vfsdata.go

Large diffs are not rendered by default.

25 changes: 10 additions & 15 deletions compiler/natives/src/crypto/rand/rand.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"errors"

"github.com/gopherjs/gopherjs/js"
"github.com/gopherjs/gopherjs/js/webcrypto"
)

func init() {
Expand All @@ -19,22 +20,16 @@ func (r *rngReader) Read(b []byte) (n int, err error) {
offset := js.InternalObject(b).Get("$offset").Int()

// browser
crypto := js.Global.Get("crypto")
if crypto == js.Undefined {
crypto = js.Global.Get("msCrypto")
}
if crypto != js.Undefined {
if crypto.Get("getRandomValues") != js.Undefined {
n = len(b)
if n > 65536 {
// Avoid QuotaExceededError thrown by getRandomValues
// when length is more than 65536, as specified in
// http://www.w3.org/TR/WebCryptoAPI/#Crypto-method-getRandomValues
n = 65536
}
crypto.Call("getRandomValues", array.Call("subarray", offset, offset+n))
return n, nil
if webcrypto.Crypto != nil && webcrypto.Crypto.Get("getRandomValues") != js.Undefined {
n = len(b)
if n > 65536 {
// Avoid QuotaExceededError thrown by getRandomValues
// when length is more than 65536, as specified in
// http://www.w3.org/TR/WebCryptoAPI/#Crypto-method-getRandomValues
n = 65536
}
webcrypto.Crypto.Call("getRandomValues", array.Call("subarray", offset, offset+n))
return n, nil
}

// Node.js
Expand Down
38 changes: 38 additions & 0 deletions compiler/natives/src/crypto/sha256/sha256.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// +build js

package sha256

import (
"errors"

"github.com/gopherjs/gopherjs/js"
"github.com/gopherjs/gopherjs/js/webcrypto"
)

var errCryptoWrongLength = errors.New("crypto: Unexpected hash length")

func webCryptoSum256(data []byte) (res [Size]byte, err error) {
jsArray, err := webcrypto.SubtleCall("digest", js.M{"name": "SHA-256"}, data)
if err != nil {
return res, err
}

slice := webcrypto.GetBytes(jsArray)
if len(slice) != Size {
return res, errCryptoWrongLength
}
copy(res[:], webcrypto.GetBytes(jsArray))
return res, err
}

//gopherjs:keep_overridden
// This function overrides the original function in the standard library
func Sum256(data []byte) [Size]byte {
res, err := webCryptoSum256(data)
if err != nil {
// The WebCryptoAPI call failed: fallback by calling the implementation from
// the Go standard library
return _gopherjs_overridden_Sum256(data)
}
return res
}
144 changes: 143 additions & 1 deletion compiler/natives/src/crypto/x509/x509.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,150 @@

package x509

import "errors"
import (
"crypto"
"crypto/ecdsa"
"crypto/rsa"
"encoding/asn1"
"encoding/base64"
"errors"
"math/big"
"os"

"github.com/gopherjs/gopherjs/js"
"github.com/gopherjs/gopherjs/js/webcrypto"
)

var (
errSignatureCheckFailed = errors.New("x509: signature check failed")
)

// This function overrides the original function in the standard library
func loadSystemRoots() (*CertPool, error) {
return nil, errors.New("crypto/x509: system root pool is not available in GopherJS")
}

// This function overrides the original function in the standard library
func execSecurityRoots() (*CertPool, error) {
return nil, os.ErrNotExist
}

func padLeft(buf []byte, length int) []byte {
res := make([]byte, length)
copy(res[length-len(buf):], buf)
return res
}

// Converts a Go crypto.PublicKey to a JavaScript CryptoKey
func pubKey2CryptoKey(publicKey crypto.PublicKey) (*js.Object, error) {
format := "jwk"
// Json Web Key (https://tools.ietf.org/html/rfc7517, https://tools.ietf.org/html/rfc7518)
var jwkKey, algorithm js.M

switch pub := publicKey.(type) {
case *ecdsa.PublicKey:
order := len(pub.Params().P.Bytes())
paddedX := padLeft(pub.X.Bytes(), order)
paddedY := padLeft(pub.Y.Bytes(), order)
jwkKey = js.M{
"kty": "EC",
"crv": pub.Params().Name,
"x": base64.RawURLEncoding.EncodeToString(paddedX),
"y": base64.RawURLEncoding.EncodeToString(paddedY),
}

algorithm = js.M{
"name": "ECDSA",
"namedCurve": pub.Params().Name,
}
case *rsa.PublicKey:
jwkKey = js.M{
"kty": "RSA",
"n": base64.RawURLEncoding.EncodeToString(pub.N.Bytes()),
"e": base64.RawURLEncoding.EncodeToString(big.NewInt(int64(pub.E)).Bytes()),
}

algorithm = js.M{"name": "RSA"}
default:
return nil, ErrUnsupportedAlgorithm
}

return webcrypto.SubtleCall("importKey", format, jwkKey, algorithm, false, []string{"verify"})
}

func webCryptoCheckSignature(algo SignatureAlgorithm, signed, signature []byte, publicKey crypto.PublicKey) error {
cryptoKey, err := pubKey2CryptoKey(publicKey)
if err != nil {
return err
}
var algoName, hashAlgoName string
var webCryptoSig []byte

switch algo {
case ECDSAWithSHA1, SHA1WithRSA:
hashAlgoName = "SHA-1"
case ECDSAWithSHA256, SHA256WithRSA:
hashAlgoName = "SHA-256"
case ECDSAWithSHA384, SHA384WithRSA:
hashAlgoName = "SHA-384"
case ECDSAWithSHA512, SHA512WithRSA:
hashAlgoName = "SHA-512"
default:
return ErrUnsupportedAlgorithm
}

switch pub := publicKey.(type) {
case *ecdsa.PublicKey:
algoName = "ECDSA"

// We have a ASN1 encoded signature and Web Crypto needs the concatenated padded valued of r and s
sigStruct := new(ecdsaSignature)
_, err = asn1.Unmarshal(signature, sigStruct)
if err != nil {
return err
}
order := len(pub.Params().P.Bytes())
r := padLeft(sigStruct.R.Bytes(), order)
s := padLeft(sigStruct.S.Bytes(), order)
webCryptoSig = append(r, s...)

case *rsa.PublicKey:
algoName = "RSA"
webCryptoSig = signature

default:
return ErrUnsupportedAlgorithm
}

algorithm := js.M{
"name": algoName,
"hash": js.M{"name": hashAlgoName},
}

res, err := webcrypto.SubtleCall("verify", algorithm, cryptoKey, webCryptoSig, signed)

if err != nil {
return err
}
if !res.Bool() {
return errSignatureCheckFailed
}
return nil
}

//gopherjs:keep_overridden
// This function overrides the original function in the standard library
func checkSignature(algo SignatureAlgorithm, signed, signature []byte, publicKey crypto.PublicKey) error {
err := webCryptoCheckSignature(algo, signed, signature, publicKey)
if err == errSignatureCheckFailed {
// WebCrypto said that the signature is not OK: fail
return err
}
if err == nil {
// WebCrypto said that the signature is OK: success
return nil
}
// WebCrypto failed for another reason: fallback to the standard go implementation
err = _gopherjs_overridden_checkSignature(algo, signed, signature, publicKey)
return err
}
Loading