java - 如何在JNA中映射Windows API CredWrite/CredRead?

标签 java winapi jna credentials

我正在尝试在 JNA 中映射 CredWrite/CredRead,以便在 Windows 凭据管理器(操作系统 Windows 10)中存储我的 Java 应用程序中使用的第三方凭据。

这是 C 中的原始签名:

// https://msdn.microsoft.com/en-us/library/aa375187(v=vs.85).aspx
BOOL CredWrite(
  _In_ PCREDENTIAL Credential,
  _In_ DWORD       Flags
);

// https://msdn.microsoft.com/en-us/library/aa374804(v=vs.85).aspx
BOOL CredRead(
  _In_  LPCTSTR     TargetName,
  _In_  DWORD       Type,
  _In_  DWORD       Flags,
  _Out_ PCREDENTIAL *Credential
);

typedef struct _CREDENTIAL {
  DWORD                 Flags;
  DWORD                 Type;
  LPTSTR                TargetName;
  LPTSTR                Comment;
  FILETIME              LastWritten;
  DWORD                 CredentialBlobSize;
  LPBYTE                CredentialBlob;
  DWORD                 Persist;
  DWORD                 AttributeCount;
  PCREDENTIAL_ATTRIBUTE Attributes;
  LPTSTR                TargetAlias;
  LPTSTR                UserName;
} CREDENTIAL, *PCREDENTIAL;

typedef struct _CREDENTIAL_ATTRIBUTE {
  LPTSTR Keyword;
  DWORD  Flags;
  DWORD  ValueSize;
  LPBYTE Value;
} CREDENTIAL_ATTRIBUTE, *PCREDENTIAL_ATTRIBUTE;

这是我的 Java map :

WinCrypt instance = (WinCrypt) Native.loadLibrary("Advapi32", WinCrypt.class, W32APIOptions.DEFAULT_OPTIONS);

public boolean CredWrite(
        CREDENTIAL.ByReference Credential,
        int Flags
        );

public boolean CredRead(
        String TargetName,
        int Type,
        int Flags,
        PointerByReference Credential
        );

public static class CREDENTIAL extends Structure {
    public int Flags;
    public int Type;
    public String TargetName;
    public String Comment;
    public FILETIME LastWritten;
    public int CredentialBlobSize;
    public byte[] CredentialBlob = new byte[128];
    public int Persist;
    public int AttributeCount;
    public CREDENTIAL_ATTRIBUTE.ByReference Attributes;
    public String TargetAlias;
    public String UserName;

    public static class ByReference extends CREDENTIAL implements Structure.ByReference {
        public ByReference() {
        }

        public ByReference(Pointer memory) {
            super(memory);                      // LINE 55
        }
    }

    public CREDENTIAL() {
        super();
    }

    public CREDENTIAL(Pointer memory) {
        super(memory); 
        read();                                 // LINE 65
    }

    @Override
    protected List<String> getFieldOrder() {
        return Arrays.asList(new String[] {
                "Flags",
                "Type",
                "TargetName",
                "Comment",
                "LastWritten",
                "CredentialBlobSize",
                "CredentialBlob",
                "Persist",
                "AttributeCount",
                "Attributes",
                "TargetAlias",
                "UserName"
        });
    }
}

public static class CREDENTIAL_ATTRIBUTE extends Structure {
    public String Keyword;
    public int Flags;
    public int ValueSize;
    public byte[] Value = new byte[128];

    public static class ByReference extends CREDENTIAL_ATTRIBUTE implements Structure.ByReference {
    }

    @Override
    protected List<String> getFieldOrder() {
        return Arrays.asList(new String[] {
                "Keyword",
                "Flags",
                "ValueSize",
                "Value"
        });
    }
}

首先,我尝试向 Windows 凭据管理器写入凭据:

String password = "passwordtest";
int cbCreds = 1 + password.length();

CREDENTIAL.ByReference credRef = new CREDENTIAL.ByReference();
credRef.Type = WinCrypt.CRED_TYPE_GENERIC;
credRef.TargetName = "TEST/account";
credRef.CredentialBlobSize = cbCreds;
credRef.CredentialBlob = password.getBytes();
credRef.Persist = WinCrypt.CRED_PERSIST_LOCAL_MACHINE;
credRef.UserName = "administrator";

boolean ok = WinCrypt.instance.CredWrite(credRef, 0);
int rc = Kernel32.INSTANCE.GetLastError();
String errMsg = Kernel32Util.formatMessage(rc);
System.out.println("CredWrite() - ok: " + ok + ", errno: " + rc + ", errmsg: " + errMsg);

尝试写入的输出:

CredWrite() - ok: false, errno: 87, errmsg: The parameter is incorrect.

然后我尝试从 Windows 凭据管理器读取现有凭据:

PointerByReference pref = new PointerByReference();
boolean ok = WinCrypt.instance.CredRead("build-apps", WinCrypt.CRED_TYPE_DOMAIN_PASSWORD, 0, pref);
int rc = Kernel32.INSTANCE.GetLastError();
String errMsg = Kernel32Util.formatMessage(rc);
System.out.println("CredRead() - ok: " + ok + ", errno: " + rc + ", errmsg: " + errMsg);
CREDENTIAL cred = new CREDENTIAL.ByReference(pref.getPointer());        // LINE 44

尝试读取的输出:

CredRead() - ok: true, errno: 0, errmsg: The operation completed successfully.
Exception in thread "main" java.lang.IllegalArgumentException: Structure exceeds provided memory bounds
    at com.sun.jna.Structure.ensureAllocated(Structure.java:366)
    at com.sun.jna.Structure.ensureAllocated(Structure.java:346)
    at com.sun.jna.Structure.read(Structure.java:552)
    at com.abc.crypt.WinCrypt$CREDENTIAL.<init>(WinCrypt.java:65)
    at com.abc.crypt.WinCrypt$CREDENTIAL$ByReference.<init>(WinCrypt.java:55) 
    at com.abc.crypt.CryptTest.main(CryptTest.java:44)
Caused by: java.lang.IndexOutOfBoundsException: Bounds exceeds available space : size=8, offset=200
    at com.sun.jna.Memory.boundsCheck(Memory.java:203)
    at com.sun.jna.Memory$SharedMemory.boundsCheck(Memory.java:87)
    at com.sun.jna.Memory.share(Memory.java:131)
    at com.sun.jna.Structure.ensureAllocated(Structure.java:363)
    ... 5 more

因此,尝试写入失败,尝试读取成功,但未能根据输出创建 CREDENTIAL 对象。

根据CredWrite API的网页,我在写测试中得到的errno 87是以下错误:

ERROR_INVALID_PARAMETER

Certain fields cannot be changed in an existing credential. This error is returned if a field does not match the value in a protected field of the existing credential.

但是,我在 CREDENTIAL 实例中输入的值是一个新的凭据,而不是 Windows 凭据管理器中的现有凭据。

任何有关如何修复/改进的建议或想法都值得赞赏。

======================================

应用修复后更新:

新信用阅读:

public boolean CredRead(
        String TargetName,
        int Type,
        int Flags,
        CREDENTIAL.ByReference Credential
        );

CredRead 测试:

CREDENTIAL.ByReference pref = new CREDENTIAL.ByReference();
boolean ok = WinCrypt.instance.CredRead("TEST/account", WinCrypt.CRED_TYPE_GENERIC, 0, pref);
int rc = Kernel32.INSTANCE.GetLastError();
String errMsg = Kernel32Util.formatMessage(rc);
System.out.println("CredRead() - ok: " + ok + ", errno: " + rc + ", errmsg: " + errMsg);
System.out.println(String.format("Read username = '%s', password='%S' (%d bytes)\n",
        pref.UserName, pref.CredentialBlob, pref.CredentialBlobSize));

结果:

CredRead() - ok: true, errno: 0, errmsg: The operation completed successfully.
Read username = 'null', password='NULL' (0 bytes)

我检查了 contrib 中的 JNA 示例如何在 out arg 上使用 ByReference,它们通过新建 ByReference 并传递给函数以相同的方式执行操作。

最佳答案

如果您查看 CredRead() 的 WIN32 定义,您会发现第四个参数的类型为 PCREDENTIAL*,即它是一个指向指针的指针。所以...

  • 您需要传入一个指针的地址,即一个 4 字节的内存块。
  • Windows 分配一 block 内存来保存 CREDENTIAL 结构,然后通过将该新内存块的地址放入您传入的 4 字节 block 中来告诉您它的位置。
  • 当您取消引用原始指针(传递给 CredRead() 的指针)时,您会得到另一个指针(4 字节 block ),该指针本身需要取消引用才能访问凭据。

欢迎来到 C:-)

TL;DR:CREDENTIAL 类需要这样定义:

public static class CREDENTIAL extends Structure {
    public int Flags;
    public int Type;
    public WString TargetName;
    public WString Comment;
    public FILETIME LastWritten;
    public int CredentialBlobSize;
    public Pointer CredentialBlob; // <== discussed below
    public int Persist;
    public int AttributeCount;
    public Pointer Attributes;
    public WString TargetAlias;
    public WString UserName;
    private Pointer RawMemBlock; // <== discussed below

    public CREDENTIAL() { }

    public CREDENTIAL( Pointer ptr ) 
    { 
        // initialize ourself from the raw memory block returned to us by ADVAPI32
        super( ptr ) ; 
        RawMemBlock = ptr ; 
        read() ;
    }

    @Override
    protected void finalize()
    {    
        // clean up
        WinCrypt.INSTANCE.CredFree( RawMemBlock ) ;
    }

    @Override
    protected List<String> getFieldOrder()
    {
        return Arrays.asList( new String[] { "Flags" , "Type" , "TargetName" , "Comment" , "LastWritten" , "CredentialBlobSize" , "CredentialBlob" , "Persist" , "AttributeCount" , "Attributes" , "TargetAlias" , "UserName" } ) ;
    }
} ;

要调用CredRead(),请按如下方式声明:

public boolean CredRead( String target , int type , int flags , PointerByReference cred ) ;

并像这样调用它:

PointerByReference pptr = new PointerByReference() ;
boolean rc = WinCrypt.INSTANCE.CredRead( target , credType , 0 , pptr ) ;
if ( ! rc )
    ... ; // handle the error
CREDENTIAL cred = new CREDENTIAL( pptr.getValue() ) ;
String userName = cred.UserName.toString() ;
String password = new String( cred.CredentialBlob.getByteArray(0,cred.CredentialBlobSize) , "UTF-16LE" ) ;

凭据 Blob 是 Windows 分配的另一 block 内存,因此您不需要自己分配它,Windows 会分配它,并通过将其地址放入 CredentialBlob 字段来告诉您它在哪里。

由于 Windows 已经为您分配了这些内存块,而且它无法知道您何时会用完它们,因此您有责任释放它们。因此,CREDENTIAL 构造函数保留 CredRead() 给它的原始指针的副本,并在终结器中调用 CredFree() 来释放该内存。 CredFree() 声明如下:

public void CredFree( Pointer cred ) ;

要保存凭据,您需要按照 CredWrite() 期望的方式准备凭据 blob,即在 CREDENTIAL.CredentialBlob 字段中存储指向它的指针:

// prepare the credential blob
byte[] credBlob = password.getBytes( "UTF-16LE" ) ;
Memory credBlobMem = new Memory( credBlob.length ) ;
credBlobMem.write( 0 , credBlob , 0 , credBlob.length ) ;

// create the credential
CREDENTIAL cred = new CREDENTIAL() ;
cred.Type = CRED_TYPE_GENERIC ;
cred.TargetName = new WString( target ) ;
cred.CredentialBlobSize = (int) credBlobMem.size() ;
cred.CredentialBlob = credBlobMem ;
cred.Persist = CRED_PERSIST_LOCAL_MACHINE ;
cred.UserName = new WString( userName ) ;

// save the credential
boolean rc = WinCrypt.INSTANCE.CredWrite( cred , 0 ) ;
if ( ! rc )
    ... ; // handle the error

作为附录,如果在服务帐户或任何其他没有永久配置文件的帐户下运行,所有这些都会遇到问题。我需要使用没有交互式登录权限的服务帐户对通过任务计划程序运行的作业执行此操作,结果是:

  • 我创建了一个设置密码的批处理文件,并通过任务计划程序运行它(以便它在服务帐户下运行,并且密码进入正确的存储)
  • Windows 创建一个临时配置文件(检查事件日志)并将密码放入其中。
  • 另一个转储密码的批处理文件显示密码已成功设置。
  • 运行主作业是有效的,因为临时配置文件仍然存在,但 5 或 10 分钟后,Windows 会删除它,包括您设置的密码:-/,因此下次运行主作业时,它会删除它。失败,因为密码不再存在。

解决方案是创建一个永久的配置文件,最好通过交互式登录,只需完成一次。如果你做不到这一点,也可以做到programmatically尽管您需要管理员权限。

关于java - 如何在JNA中映射Windows API CredWrite/CredRead?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/38404517/

相关文章:

.net - 如何在 vb.net 中创建辅助消息循环?

delphi - 如何使用 CryptoAPI 加载 OpenSSL 生成的 RSA1024 纯文本公钥?

java - 将 C++ 库转换为 .so/.dll 共享库

java - 使用Java原生访问更改MAC系统桌面背景

java - 将 Apache HttpComponents 用于 https 请求 : "peer not authenticated" and "handshake_failure" errors

Java OutOfMemoryError 没有抛出

windows - 在 Go 和 Windows API 中处理未确定大小的数组

java - 无法在java中实例化结构内部的结构

java - 我应该将凭证 : application. 属性与系统环境变量放在哪里

java - 带美元符号的字符串模板