' Gambas module file

' https://tools.ietf.org/html/draft-josefsson-scrypt-kdf-00#page-10
Private Const PBKDF2_SHA256_80000_64 As String = ""
    "\x4d\xdc\xd8\xf6\x0b\x98\xbe\x21\x83\x0c\xee\x5e\xf2\x27\x01\xf9"
    "\x64\x1a\x44\x18\xd0\x4c\x04\x14\xae\xff\x08\x87\x6b\x34\xab\x56"
    "\xa1\xd4\x25\xa1\x22\x58\x33\x54\x9a\xdb\x84\x1b\x51\xc9\xb3\x17"
    "\x6a\x27\x2b\xde\xbb\xa1\xd0\x78\x47\x8f\x62\xb3\x97\xf3\x3c\x8d"

Private Const SCRYPT_1024_8_64 As String = ""
    "\xfd\xba\xbe\x1c\x9d\x34\x72\x00\x78\x56\xe7\x19\x0d\x01\xe9\xfe"
    "\x7c\x6a\xd7\xcb\xc8\x23\x78\x30\xe7\x73\x76\x63\x4b\x37\x31\x62"
    "\x2e\xaf\x30\xd9\x2e\x22\xa3\x88\x6f\xf1\x09\x27\x9d\x98\x30\xda"
    "\xc7\x27\xaf\xb9\x4a\x83\xee\x6d\x83\x60\xcb\xdf\xa2\xcc\x06\x40"

Private Function Base64UrlDecode(s As String) As String
  s = Replace(s, "-", "+")
  s = Replace(s, "_", "/")
  While Len(s) Mod 4 <> 0
    s &= "="
  Wend
  Return UnBase64$(s)
End

Public Sub AssertContains(arr As String[], target As String, message As String)
  Dim s As String
  For Each s In arr
    If s = target Then
      Assert.Ok(True, message)
      Return
    End If
  Next
  Assert.Ok(False, message)
End

Public Sub Test_PKey_Generate_Returns2048BitKey()
  Dim k As PKey
  k = PKey.Generate(2048)
  Assert.NotNull(k, "Generate returns an object")
  Assert.Equals(k.Type, "RSA", "Generated key has Type RSA")
  Assert.Equals(k.Bits, 2048, "Generated key has 2048 bits")
  Assert.Equals(k.IsPrivate, True, "Generated key has private material")
End

Public Sub Test_PKey_PemRoundTrip()
  Dim k1, k2 As PKey
  Dim pem1, pem2 As String

  k1 = PKey.Generate(2048)
  pem1 = k1.ToPem()
  Assert.Ok(InStr(pem1, "-----BEGIN") > 0, "ToPem produces a PEM blob")

  k2 = PKey.LoadPrivate(pem1)
  pem2 = k2.ToPem()
  Assert.Equals(pem2, pem1, "Round-trip PEM is stable")
  Assert.Equals(k2.Bits, 2048, "Loaded key has same bit length")
End

Public Sub Test_PKey_EncryptedPemRoundTrip()
  Dim k1, k2 As PKey
  Dim pem As String
  Dim passphrase As String = "correct horse battery staple"

  k1 = PKey.Generate(2048)
  pem = k1.ToPem(passphrase)
  Assert.Ok(InStr(pem, "ENCRYPTED") > 0, "Encrypted PEM contains ENCRYPTED marker")

  k2 = PKey.LoadPrivate(pem, passphrase)
  Assert.Equals(k2.Bits, 2048, "Loaded encrypted key has 2048 bits")
End

Public Sub Test_PKey_LoadPrivate_WrongPassphraseRaisesError()
  Dim k As PKey = PKey.Generate(2048)
  Dim pem As String = k.ToPem("right")
  Try k = PKey.LoadPrivate(pem, "wrong")
  Assert.Error("Loading with wrong passphrase raises an error")
End

Public Sub Test_PKey_PublicPemRoundTrip()
  Dim k As PKey = PKey.Generate(2048)
  Dim pubPem As String = k.ToPublicPem()
  Assert.Ok(InStr(pubPem, "BEGIN PUBLIC KEY") > 0, "ToPublicPem produces SPKI PEM")

  Dim k2 As PKey = PKey.LoadPublic(pubPem)
  Assert.Equals(k2.Bits, 2048, "Loaded public key has correct bits")
  Assert.NotOk(k2.IsPrivate, "Loaded public key is not private")
End

Public Sub Test_PKey_PublicPart_ExtractsPublic()
  Dim priv As PKey = PKey.Generate(2048)
  Assert.Ok(priv.IsPrivate, "Original is private")

  Dim pub As PKey = priv.PublicPart
  Assert.NotOk(pub.IsPrivate, "PublicPart is not private")
  Assert.Equals(pub.Bits, 2048, "PublicPart has same bits")

  ' Same public modulus → same SPKI export
  Assert.Equals(pub.ToPublicPem(), priv.ToPublicPem(), "PublicPart matches original public PEM")
End

Public Sub Test_PKey_DerRoundTrip()
  Dim k1 As PKey = PKey.Generate(2048)
  Dim der As String = k1.ToDer()
  Assert.Ok(Len(der) > 1000, "DER private key is substantial in size")

  Dim k2 As PKey = PKey.LoadPrivateDer(der)
  Assert.Ok(k2.IsPrivate, "Loaded DER private key is private")
  Assert.Equals(k2.Bits, 2048, "DER round-trip preserves Bits")
  Assert.Equals(k2.ToDer(), der, "DER round-trip is byte-stable")
End

Public Sub Test_PKey_PublicDerRoundTrip()
  Dim k1 As PKey = PKey.Generate(2048).PublicPart
  Dim der As String = k1.ToPublicDer()
  Dim k2 As PKey = PKey.LoadPublicDer(der)
  Assert.NotOk(k2.IsPrivate, "Loaded public DER is not private")
  Assert.Equals(k2.ToPublicDer(), der, "Public DER round-trip is byte-stable")
End

Public Sub Test_PKey_LoadInvalidPem_RaisesError()
  Dim k As PKey
  Try k = PKey.LoadPrivate("this is not a PEM")
  Assert.Error("Garbage input raises error")
End

Public Sub Test_PKey_GenerateZeroBits_RaisesError()
  Dim k As PKey
  Try k = PKey.Generate(0)
  Assert.Error("Generate(0) raises error")
End

Public Sub Test_PKey_ToDer_OnPublicKey_RaisesError()
  Dim pub As PKey = PKey.Generate(2048).PublicPart
  Dim der As String
  Try der = pub.ToDer()
  Assert.Error("ToDer on public-only key raises error")
End

Public Sub Test_Signature_List_ContainsExpectedMethods()
  Dim names As String[] = Signature.List
  AssertContains(names, "rsa-pkcs1-sha256", "List contains rsa-pkcs1-sha256")
  AssertContains(names, "rsa-pss-sha256",   "List contains rsa-pss-sha256")
  Assert.Ok(Signature.IsSupported("rsa-pkcs1-sha256"), "IsSupported true for known")
  Assert.NotOk(Signature.IsSupported("nope-md5"),        "IsSupported false for unknown")
End

Public Sub Test_Signature_UnknownMethod_RaisesError()
  Dim m As Object
  Try m = Signature["nope-md5"]
  Assert.Error("Unknown method raises on indexer")
End

Public Sub Test_Signature_RoundTrip()
  Dim k As PKey = PKey.Generate(2048)
  Dim data As String = "Hello, gb.openssl!"
  Dim methods As String[] = ["rsa-pkcs1-sha256", "rsa-pkcs1-sha384", "rsa-pkcs1-sha512",
                              "rsa-pss-sha256",   "rsa-pss-sha384",   "rsa-pss-sha512"]
  Dim m As String, sig As String

  For Each m In methods
    sig = Signature[m].Sign(data, k)
    Assert.Ok(Len(sig) >= 256, "Sig length reasonable for " & m)
    Assert.Ok(Signature[m].Verify(data, sig, k.PublicPart), "Round-trip Verify true for " & m)
    Assert.NotOk(Signature[m].Verify(data & "x", sig, k.PublicPart), "Tampered data: Verify false for " & m)
  Next
End

Public Sub Test_Signature_VerifyWrongKey_ReturnsFalse()
  Dim k1 As PKey = PKey.Generate(2048)
  Dim k2 As PKey = PKey.Generate(2048)
  Dim sig As String = Signature["rsa-pkcs1-sha256"].Sign("hello", k1)
  Assert.NotOk(Signature["rsa-pkcs1-sha256"].Verify("hello", sig, k2.PublicPart), "Verify with wrong public key returns FALSE")
End

Public Sub Test_Signature_VerifyWithPublicKeyForSign_RaisesError()
  Dim pub As PKey = PKey.Generate(2048).PublicPart
  Dim sig As String
  Try sig = Signature["rsa-pkcs1-sha256"].Sign("hello", pub)
  Assert.Error("Signing with public-only key raises error")
End

' RFC 7515 Appendix A.2.1 — RS256 example public key (JWK n,e converted
' to SPKI PEM via Python's cryptography library).
Private Const RFC7515_A2_PUB_PEM As String = ""
    "-----BEGIN PUBLIC KEY-----\n"
    "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAofgWCuLjybRlzo0tZWJj\n"
    "NiuSfb4p4fAkd/wWJcyQoTbji9k0l8W26mPddxHmfHQp+Vaw+4qPCJrcS2mJPMEz\n"
    "P1Pt0Bm4d4QlL+yRT+SFd2lZS+pCgNMsD1W/YpRPEwOWvG6b32690r2jZ47soMZo\n"
    "9wGzjb/7OMg0LOL+bSf63kpaSHSXndS5z5rexMdbBYUsLA9e+KXBdQOS+UTo7WTB\n"
    "EMa2R2CapHg665xsmtdVMTBQY4uDZlxvb3qCo5ZwKh9kG4LT6/I5IhlJH7aGhyxX\n"
    "FvUK+DWNmoudF8NAco9/h9iaGNj8q2ethFkMLs91kzk2PAcDTW9gb54h4FRWyuXp\n"
    "oQIDAQAB\n"
    "-----END PUBLIC KEY-----\n"

' RFC 7515 §A.2.1 — signing input (base64url(header) "." base64url(payload))
Private Const RFC7515_A2_SIGNED_INPUT As String = "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ"

' RFC 7515 §A.2.1 — signature (base64url-encoded raw RSA-SHA256 signature)
Private Const RFC7515_A2_SIGNATURE_B64URL As String = ""
    "cC4hiUPoj9Eetdgtv3hF80EGrhuB__dzERat0XF9g2VtQgr9PJbu3XOiZj5RZmh7"
    "AAuHIm4Bh-0Qc_lF5YKt_O8W2Fp5jujGbds9uJdbF9CUAr7t1dnZcAcQjbKBYNX4"
    "BAynRFdiuB--f_nZLgrnbyTyWzO75vRK5h6xBArLIARNPvkSjtQBMHlb1L07Qe7K0"
    "GarZRmB_eSN9383LcOLn6_dO--xi12jzDwusC-eOkHWEsqtFZESc6BfI7noOPqvhJ1"
    "phCnvWh6IeYI2w9QOYEUipUTI8np6LbgGY9Fs98rqVt5AXLIhWkWywlVmtVrBp0ig"
    "cN_IoypGlUPQGe77Rw"

Public Sub Test_Signature_VerifyKnownVector_Jwt()
  Dim pub As PKey = PKey.LoadPublic(RFC7515_A2_PUB_PEM)
  Dim sig As String = Base64UrlDecode(RFC7515_A2_SIGNATURE_B64URL)
  Dim ok As Boolean = Signature["rsa-pkcs1-sha256"].Verify(RFC7515_A2_SIGNED_INPUT, sig, pub)
  Assert.Ok(ok, "RFC 7515 A.2 (RS256) golden vector verifies")
End

Public Sub Test_PKeyCipher_List_ContainsExpectedMethods()
  Dim names As String[] = PKeyCipher.List
  AssertContains(names, "rsa-pkcs1",       "List contains rsa-pkcs1")
  AssertContains(names, "rsa-oaep-sha1",   "List contains rsa-oaep-sha1")
  AssertContains(names, "rsa-oaep-sha256", "List contains rsa-oaep-sha256")
  Assert.Ok(PKeyCipher.IsSupported("rsa-oaep-sha256"), "IsSupported true for known")
  Assert.NotOk(PKeyCipher.IsSupported("nope-md5"),     "IsSupported false for unknown")
End

Public Sub Test_PKeyCipher_UnknownMethod_RaisesError()
  Dim m As Object
  Try m = PKeyCipher["nope-md5"]
  Assert.Error("Unknown method raises on indexer")
End

Public Sub Test_PKeyCipher_RoundTrip()
  Dim k As PKey = PKey.Generate(2048)
  Dim data As String = "Hello, gb.openssl!"
  Dim methods As String[] = ["rsa-pkcs1", "rsa-oaep-sha1", "rsa-oaep-sha256"]
  Dim m, ct, pt As String

  For Each m In methods
    ct = PKeyCipher[m].Encrypt(data, k.PublicPart)
    Assert.Equals(Len(ct), 256, "Ciphertext is one modulus block for " & m)
    pt = PKeyCipher[m].Decrypt(ct, k)
    Assert.Equals(pt, data, "Round-trip Decrypt(Encrypt(d)) = d for " & m)
  Next
End

Public Sub Test_PKeyCipher_Pkcs1_RandomizedCiphertext()
  Dim k As PKey = PKey.Generate(2048)
  Dim ct1 As String = PKeyCipher["rsa-pkcs1"].Encrypt("hello", k.PublicPart)
  Dim ct2 As String = PKeyCipher["rsa-pkcs1"].Encrypt("hello", k.PublicPart)
  Assert.Ok(ct1 <> ct2, "PKCS#1 v1.5 uses random padding, so two encryptions of the same data differ")
End

Public Sub Test_PKeyCipher_Oaep_RandomizedCiphertext()
  Dim k As PKey = PKey.Generate(2048)
  Dim ct1 As String = PKeyCipher["rsa-oaep-sha256"].Encrypt("hello", k.PublicPart)
  Dim ct2 As String = PKeyCipher["rsa-oaep-sha256"].Encrypt("hello", k.PublicPart)
  Assert.Ok(ct1 <> ct2, "OAEP uses a random seed, so two encryptions of the same data differ")
End

Public Sub Test_PKeyCipher_BinaryDataWithNuls_RoundTrip()
  Dim k As PKey = PKey.Generate(2048)
  Dim data As String = "ABC" & Chr(0) & "DEF" & Chr(0) & Chr(0) & "GHI"
  Dim ct As String = PKeyCipher["rsa-oaep-sha256"].Encrypt(data, k.PublicPart)
  Dim pt As String = PKeyCipher["rsa-oaep-sha256"].Decrypt(ct, k)
  Assert.Equals(pt, data, "Binary data with embedded NULs round-trips intact")
End

Public Sub Test_PKeyCipher_MaxLengthData_RoundTrip()
  ' OAEP-SHA256 max plaintext = modulus_bytes - 2 - 2*hashlen = 256 - 2 - 64 = 190
  Dim k As PKey = PKey.Generate(2048)
  Dim data As String = String$(190, "A")
  Dim ct As String = PKeyCipher["rsa-oaep-sha256"].Encrypt(data, k.PublicPart)
  Dim pt As String = PKeyCipher["rsa-oaep-sha256"].Decrypt(ct, k)
  Assert.Equals(pt, data, "Maximum-length plaintext (190 bytes for OAEP-SHA256) round-trips")
End

Public Sub Test_PKeyCipher_DataTooLong_RaisesError()
  ' PKCS#1 v1.5 max plaintext = modulus_bytes - 11 = 245; 256 is too long.
  Dim k As PKey = PKey.Generate(2048)
  Dim ct As String
  Try ct = PKeyCipher["rsa-pkcs1"].Encrypt(String$(256, "A"), k.PublicPart)
  Assert.Error("Encrypting plaintext longer than modulus minus padding raises error")
End

Public Sub Test_PKeyCipher_DecryptWithWrongKey_RaisesError()
  Dim k1 As PKey = PKey.Generate(2048)
  Dim k2 As PKey = PKey.Generate(2048)
  Dim ct As String = PKeyCipher["rsa-oaep-sha256"].Encrypt("hello", k1.PublicPart)
  Dim pt As String
  Try pt = PKeyCipher["rsa-oaep-sha256"].Decrypt(ct, k2)
  Assert.Error("Decrypting with a different private key raises error")
End

Public Sub Test_PKeyCipher_DecryptWithPublicKey_RaisesError()
  Dim k As PKey = PKey.Generate(2048)
  Dim ct As String = PKeyCipher["rsa-oaep-sha256"].Encrypt("hello", k.PublicPart)
  Dim pt As String
  Try pt = PKeyCipher["rsa-oaep-sha256"].Decrypt(ct, k.PublicPart)
  Assert.Error("Decrypting with a public-only key raises error")
End

Public Sub Test_PKeyCipher_CrossPadding_RaisesError()
  Dim k As PKey = PKey.Generate(2048)
  Dim ct As String = PKeyCipher["rsa-pkcs1"].Encrypt("hello", k.PublicPart)
  Dim pt As String
  Try pt = PKeyCipher["rsa-oaep-sha256"].Decrypt(ct, k)
  Assert.Error("Decrypting a PKCS#1 ciphertext as OAEP raises error")
End

Public Sub Test_PKeyCipher_CrossOaepHash_RaisesError()
  Dim k As PKey = PKey.Generate(2048)
  Dim ct As String = PKeyCipher["rsa-oaep-sha1"].Encrypt("hello", k.PublicPart)
  Dim pt As String
  Try pt = PKeyCipher["rsa-oaep-sha256"].Decrypt(ct, k)
  Assert.Error("Decrypting an OAEP-SHA1 ciphertext as OAEP-SHA256 raises error")
End

Public Sub Test_PKeyCipher_EncryptWithPublicOnly_DecryptWithPrivate()
  Dim k As PKey = PKey.Generate(2048)
  Dim pubOnly As PKey = PKey.LoadPublic(k.ToPublicPem())
  Dim ct As String = PKeyCipher["rsa-oaep-sha256"].Encrypt("secret", pubOnly)
  Dim pt As String = PKeyCipher["rsa-oaep-sha256"].Decrypt(ct, k)
  Assert.Equals(pt, "secret", "Encrypt with imported public PEM, decrypt with original private")
End

Public Sub Test_PKeyCipher_RepeatedOperations_NoCrash()
  Dim k As PKey = PKey.Generate(2048)
  Dim i As Integer
  Dim payload, ct, pt As String

  For i = 1 To 20
    payload = OpenSSL.RandomBytes(64)
    ct = PKeyCipher["rsa-oaep-sha256"].Encrypt(payload, k.PublicPart)
    pt = PKeyCipher["rsa-oaep-sha256"].Decrypt(ct, k)
    If pt <> payload Then
      Assert.Ok(False, "Round-trip diverged at iteration " & i)
      Return
    Endif
  Next
  Assert.Ok(True, "Twenty random round-trips all match")
End

Public Sub Main()

  Dim p, s As String
  Dim a As New String[]

  Test.Plan(79)

  p = OpenSSL.Pbkdf2("Password", "NaCl", 80000, 64, "SHA256")
  s = OpenSSL.Scrypt("password", "NaCl", 1024, 8, 16, 64)
  Assert.Equals(Base64$(p), Base64$(PBKDF2_SHA256_80000_64), "PBKDF2 matches test vector")
  Assert.Equals(Base64$(s), Base64$(SCRYPT_1024_8_64), "scrypt matches test vector")

  Test.Note("Testing again for memory management")
  For i As Integer = 1 To 2 ^ 10
    a.Add(OpenSSL.RandomBytes(2 ^ 14))
  Next

  Assert.Equals(Base64$(p), Base64$(PBKDF2_SHA256_80000_64), "PBKDF2 matches test vector")
  Assert.Equals(Base64$(s), Base64$(SCRYPT_1024_8_64), "scrypt matches test vector")

  Test_PKey_Generate_Returns2048BitKey()
  Test_PKey_PemRoundTrip()
  Test_PKey_EncryptedPemRoundTrip()
  Test_PKey_LoadPrivate_WrongPassphraseRaisesError()
  Test_PKey_PublicPemRoundTrip()
  Test_PKey_PublicPart_ExtractsPublic()
  Test_PKey_DerRoundTrip()
  Test_PKey_PublicDerRoundTrip()
  Test_PKey_LoadInvalidPem_RaisesError()
  Test_PKey_GenerateZeroBits_RaisesError()
  Test_PKey_ToDer_OnPublicKey_RaisesError()

  Test_Signature_List_ContainsExpectedMethods()
  Test_Signature_UnknownMethod_RaisesError()
  Test_Signature_RoundTrip()
  Test_Signature_VerifyWrongKey_ReturnsFalse()
  Test_Signature_VerifyWithPublicKeyForSign_RaisesError()
  Test_Signature_VerifyKnownVector_Jwt()

  Test_PKeyCipher_List_ContainsExpectedMethods()
  Test_PKeyCipher_UnknownMethod_RaisesError()
  Test_PKeyCipher_RoundTrip()
  Test_PKeyCipher_Pkcs1_RandomizedCiphertext()
  Test_PKeyCipher_Oaep_RandomizedCiphertext()
  Test_PKeyCipher_BinaryDataWithNuls_RoundTrip()
  Test_PKeyCipher_MaxLengthData_RoundTrip()
  Test_PKeyCipher_DataTooLong_RaisesError()
  Test_PKeyCipher_DecryptWithWrongKey_RaisesError()
  Test_PKeyCipher_DecryptWithPublicKey_RaisesError()
  Test_PKeyCipher_CrossPadding_RaisesError()
  Test_PKeyCipher_CrossOaepHash_RaisesError()
  Test_PKeyCipher_EncryptWithPublicOnly_DecryptWithPrivate()
  Test_PKeyCipher_RepeatedOperations_NoCrash()

End
